generated from kevisual/vite-react-template
Compare commits
65 Commits
300da513c1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
518a3f2864 | ||
|
|
477826dcce | ||
|
|
ef08303182 | ||
|
|
389f7a7ad2 | ||
|
|
dd6eff9269 | ||
|
|
9a06364880 | ||
|
|
3f0733a540 | ||
|
|
330accb822 | ||
|
|
469d23b0b9 | ||
|
|
bc9ce9e5df | ||
|
|
0b08b82356 | ||
|
|
3a821b1486 | ||
|
|
81b52cce8c | ||
|
|
500ceb2e42 | ||
| a40cc2175e | |||
|
|
f74d5a4510 | ||
| 4ed81a1c68 | |||
| 253ef2ac7d | |||
| 6c914c3186 | |||
| eebdaec389 | |||
| 575feec78b | |||
|
|
d196b24d07 | ||
|
|
20edf1893e | ||
| 0a0acf1fe7 | |||
| 364e903d5f | |||
| f05aab2430 | |||
| 58563ece93 | |||
| ee176fd80b | |||
| d85fc1154f | |||
| 3227c795a3 | |||
| 0d8d81b1fc | |||
| 9e8c658159 | |||
| 60a7328889 | |||
| 245bbb33b0 | |||
| a54597c65e | |||
| 5a769a6748 | |||
| bbb762db97 | |||
| 3ef9c47508 | |||
| 635e6a8a1b | |||
| 7ec6428643 | |||
| f4643464ba | |||
| f1c27a8726 | |||
| 6b37e34392 | |||
| de87263a5d | |||
| eb1adbc100 | |||
| 6516bacbf4 | |||
| 83fc1dca18 | |||
| d85a1cf84b | |||
|
|
fe3a3cfdc3 | ||
|
|
a9f42f14f0 | ||
| a934a7620d | |||
| 6750a8858d | |||
| f9fd2a67b4 | |||
| 1884e87421 | |||
| 3d66eee666 | |||
| f876a65c6b | |||
| 36edf12fd0 | |||
| 1f5aa69aed | |||
| b90020cde0 | |||
| a2629fec7b | |||
| 0ced574b8b | |||
| 4a9bbf1911 | |||
| f117302a98 | |||
| abd9860a90 | |||
| 234dabcfb4 |
30
.cnb.yml
30
.cnb.yml
@@ -4,8 +4,7 @@ include:
|
||||
|
||||
.common_env: &common_env
|
||||
env:
|
||||
TO_REPO: template/vite-react-template
|
||||
TO_URL: git.xiongxiao.me
|
||||
USERNAME: root
|
||||
imports:
|
||||
- https://cnb.cool/kevisual/env/-/blob/main/.env.development
|
||||
|
||||
@@ -16,29 +15,6 @@ $:
|
||||
services:
|
||||
- vscode
|
||||
- docker
|
||||
env: !reference [.common_env, env]
|
||||
imports: !reference [.common_env, imports]
|
||||
# 开发环境启动后会执行的任务
|
||||
# stages:
|
||||
# - name: pnpm install
|
||||
# script: pnpm install
|
||||
stages: !reference [.dev_template, stages]
|
||||
|
||||
.common_sync_to_gitea: &common_sync_to_gitea
|
||||
- <<: *common_env
|
||||
services: !reference [.common_sync_to_gitea_template, services]
|
||||
stages: !reference [.common_sync_to_gitea_template, stages]
|
||||
|
||||
.common_sync_from_gitea: &common_sync_from_gitea
|
||||
- <<: *common_env
|
||||
services: !reference [.common_sync_from_gitea_template, services]
|
||||
stages: !reference [.common_sync_from_gitea_template, stages]
|
||||
|
||||
main:
|
||||
web_trigger_sync_to_gitea:
|
||||
- <<: *common_sync_to_gitea
|
||||
web_trigger_sync_from_gitea:
|
||||
- <<: *common_sync_from_gitea
|
||||
api_trigger_sync_to_gitea:
|
||||
- <<: *common_sync_to_gitea
|
||||
api_trigger_sync_from_gitea:
|
||||
- <<: *common_sync_from_gitea
|
||||
stages: !reference [.dev_template, stages]
|
||||
@@ -1,7 +1,7 @@
|
||||
## 本地环境
|
||||
|
||||
# VITE_API_URL = "http://localhost:8000"
|
||||
# VITE_API_URL="http://localhost:8000"
|
||||
### 开发环境
|
||||
VITE_API_URL = "https://kevisual.xiongxiao.me"
|
||||
VITE_API_URL="https://kevisual.xiongxiao.me"
|
||||
### 生产环境
|
||||
# VITE_API_URL = "https://kevisual.cn"
|
||||
# VITE_API_UR="https://kevisual.cn"
|
||||
|
||||
26
.gitignore
vendored
26
.gitignore
vendored
@@ -1,37 +1,19 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
.env
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
pack-dist
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
tsconfig.app.tsbuildinfo
|
||||
tsconfig.node.tsbuildinfo
|
||||
|
||||
.turbo
|
||||
|
||||
.pnpm-store
|
||||
|
||||
.tanstack
|
||||
.env
|
||||
.env*
|
||||
|
||||
!.env.example
|
||||
!.env.example
|
||||
.pnpm-lock.yaml
|
||||
72
AGENTS.md
Normal file
72
AGENTS.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# AGENTS.md
|
||||
|
||||
本指南为在此仓库中工作的 AI 编码代理提供关键信息。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/ui/ # shadcn/ui 组件(Base UI 基础组件)
|
||||
├── lib/ # 工具函数(cn() 函数用于 className 合并)
|
||||
├── modules/ # 应用模块(query client、basename)
|
||||
├── pages/ # 页面组件(默认导出)
|
||||
├── routes/ # TanStack Router 基于文件的路由
|
||||
├── styles/ # 全局样式、主题 CSS
|
||||
└── main.tsx # 应用入口
|
||||
```
|
||||
|
||||
|
||||
## 代码风格指南
|
||||
|
||||
### 模块目录结构
|
||||
|
||||
每个新模块(如 `page-app`)应遵循以下结构:
|
||||
|
||||
```
|
||||
pages/page-app/
|
||||
├── components/ # 模块专属组件
|
||||
├── hooks/ # 模块 React Query hooks(API 查询封装)
|
||||
├── modules/ # 模块功能函数(UI 组件、工具函数等)
|
||||
└── store/ # 模块状态管理(Zustand)
|
||||
```
|
||||
|
||||
### hooks/ 文件夹说明
|
||||
|
||||
每个模块的 `hooks/` 文件夹用于封装与该模块相关的 React Query hooks:
|
||||
|
||||
- **use-api-query.ts**: 使用 `@tanstack/react-query` 的 `useQuery` 封装 API 调用
|
||||
- 定义 `queryKeys` 常量用于缓存标识
|
||||
- 封装 `useQuery` hooks 用于数据获取(GET 请求)
|
||||
- 封装 `useMutation` hooks 用于数据修改(POST/PUT/DELETE 请求)
|
||||
- 支持预取(prefetch)和无限滚动(infinite query)
|
||||
- **index.ts**: 导出模块所有 hooks,便于统一导入使用
|
||||
|
||||
### 状态和数据获取
|
||||
|
||||
- **@tanstack/react-query** 用于数据获取、缓存和状态管理
|
||||
- 在模块的 `hooks/` 文件夹中封装 API 调用
|
||||
- QueryClient 实例位于 `src/modules/query.ts`
|
||||
- 在 `src/routes/__root.tsx` 中通过 `QueryClientProvider` 提供
|
||||
- **Zustand** 用于全局状态管理
|
||||
- **@kevisual/query** 用于底层 API 请求封装
|
||||
- **React Hook Form** 用于表单管理
|
||||
|
||||
## 核心依赖
|
||||
|
||||
- **@base-ui/react**: Headless UI 基础组件
|
||||
- **@tanstack/react-query**: 数据获取、缓存和状态管理(配合 hooks/ 使用)
|
||||
- **@tanstack/react-router**: 基于 TanStack Router 插件的文件路由
|
||||
- **class-variance-authority**: 基于变体的样式系统
|
||||
- **clsx + tailwind-merge**: 通过 `cn()` 提供 className 工具函数
|
||||
- **lucide-react**: 图标库
|
||||
- **react-hook-form**: 表单处理
|
||||
- **sonner**: Toast 通知
|
||||
- **zustand**: 状态管理
|
||||
- **tailwindcss v4**: 使用 @tailwindcss/vite 插件进行样式处理
|
||||
|
||||
## 主题系统
|
||||
|
||||
- **主题配色**: 采用黑白配色方案,提供简洁优雅的视觉体验
|
||||
- **主题模式**: 支持 light(浅色)和 dark(深色)模式切换
|
||||
- **主题实现**: 使用 `next-themes` 进行主题管理
|
||||
- **CSS 变量**: 主题相关的 CSS 变量定义在 `src/styles/theme.css` 中
|
||||
13
README.md
13
README.md
@@ -1 +1,12 @@
|
||||
# vite-react-template
|
||||
# cnb center
|
||||
|
||||
一个应用工作台
|
||||
|
||||
## 功能
|
||||
|
||||
1. 对话管理
|
||||
2. 仓库管理
|
||||
3. 云开发(cloud-dev)
|
||||
4. 应用管理
|
||||
5. Agent管理
|
||||
6. 配置
|
||||
761
bun.lock
Normal file
761
bun.lock
Normal file
@@ -0,0 +1,761 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "@kevisual/cnb-center",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^3.0.58",
|
||||
"@ai-sdk/openai": "^3.0.41",
|
||||
"@ai-sdk/openai-compatible": "^2.0.35",
|
||||
"@base-ui/react": "^1.3.0",
|
||||
"@kevisual/api": "^0.0.64",
|
||||
"@kevisual/cnb": "^0.0.45",
|
||||
"@kevisual/cnb-ai": "^0.0.2",
|
||||
"@kevisual/context": "^0.0.8",
|
||||
"@kevisual/kv-login": "^0.1.17",
|
||||
"@kevisual/router": "0.1.1",
|
||||
"@tanstack/react-router": "^1.166.7",
|
||||
"@uiw/react-codemirror": "^4.25.8",
|
||||
"ai": "^6.0.116",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"dayjs": "^1.11.20",
|
||||
"es-toolkit": "^1.45.1",
|
||||
"fuse.js": "^7.1.0",
|
||||
"idb-keyval": "^6.2.2",
|
||||
"lucide-react": "^0.577.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-hook-form": "^7.71.2",
|
||||
"sonner": "^2.0.7",
|
||||
"zod": "^4.3.6",
|
||||
"zustand": "^5.0.11",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codemirror/lang-yaml": "^6.1.2",
|
||||
"@kevisual/gitea": "^0.0.6",
|
||||
"@kevisual/query": "0.0.53",
|
||||
"@kevisual/types": "^0.0.12",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"@tanstack/react-router-devtools": "^1.166.7",
|
||||
"@tanstack/router-plugin": "^1.166.7",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.0",
|
||||
"dotenv": "^17.3.1",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^8.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.58", "https://registry.npmmirror.com/@ai-sdk/anthropic/-/anthropic-3.0.58.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/53SACgmVukO4bkms4dpxpRlYhW8Ct6QZRe6sj1Pi5H00hYhxIrqfiLbZBGxkdRvjsBQeP/4TVGsXgH5rQeb8Q=="],
|
||||
|
||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.66", "https://registry.npmmirror.com/@ai-sdk/gateway/-/gateway-3.0.66.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-SIQ0YY0iMuv+07HLsZ+bB990zUJ6S4ujORAh+Jv1V2KGNn73qQKnGO0JBk+w+Res8YqOFSycwDoWcFlQrVxS4A=="],
|
||||
|
||||
"@ai-sdk/openai": ["@ai-sdk/openai@3.0.41", "https://registry.npmmirror.com/@ai-sdk/openai/-/openai-3.0.41.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-IZ42A+FO+vuEQCVNqlnAPYQnnUpUfdJIwn1BEDOBywiEHa23fw7PahxVtlX9zm3/zMvTW4JKPzWyvAgDu+SQ2A=="],
|
||||
|
||||
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.35", "https://registry.npmmirror.com/@ai-sdk/openai-compatible/-/openai-compatible-2.0.35.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-g3wA57IAQFb+3j4YuFndgkUdXyRETZVvbfAWM+UX7bZSxA3xjes0v3XKgIdKdekPtDGsh4ZX2byHD0gJIMPfiA=="],
|
||||
|
||||
"@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "https://registry.npmmirror.com/@ai-sdk/provider/-/provider-3.0.8.tgz", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
|
||||
|
||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "https://registry.npmmirror.com/@ai-sdk/provider-utils/-/provider-utils-4.0.19.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="],
|
||||
|
||||
"@babel/code-frame": ["@babel/code-frame@7.29.0", "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.0.tgz", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.29.0", "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.0.tgz", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
|
||||
|
||||
"@babel/core": ["@babel/core@7.29.0", "https://registry.npmmirror.com/@babel/core/-/core-7.29.0.tgz", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
|
||||
|
||||
"@babel/generator": ["@babel/generator@7.29.1", "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.1.tgz", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
|
||||
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
|
||||
|
||||
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
||||
|
||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
|
||||
|
||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
|
||||
|
||||
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||
|
||||
"@babel/helpers": ["@babel/helpers@7.28.6", "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.28.6.tgz", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.29.0", "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.0.tgz", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
||||
|
||||
"@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "https://registry.npmmirror.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="],
|
||||
|
||||
"@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "https://registry.npmmirror.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="],
|
||||
|
||||
"@babel/runtime": ["@babel/runtime@7.28.6", "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.6.tgz", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
|
||||
|
||||
"@babel/template": ["@babel/template@7.28.6", "https://registry.npmmirror.com/@babel/template/-/template-7.28.6.tgz", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.29.0", "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.0.tgz", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.29.0", "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||
|
||||
"@base-ui/react": ["@base-ui/react@1.3.0", "https://registry.npmmirror.com/@base-ui/react/-/react-1.3.0.tgz", { "dependencies": { "@babel/runtime": "^7.28.6", "@base-ui/utils": "0.2.6", "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "tabbable": "^6.4.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-FwpKqZbPz14AITp1CVgf4AjhKPe1OeeVKSBMdgD10zbFlj3QSWelmtCMLi2+/PFZZcIm3l87G7rwtCZJwHyXWA=="],
|
||||
|
||||
"@base-ui/utils": ["@base-ui/utils@0.2.6", "https://registry.npmmirror.com/@base-ui/utils/-/utils-0.2.6.tgz", { "dependencies": { "@babel/runtime": "^7.28.6", "@floating-ui/utils": "^0.2.11", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-yQ+qeuqohwhsNpoYDqqXaLllYAkPCP4vYdDrVo8FQXaAPfHWm1pG/Vm+jmGTA5JFS0BAIjookyapuJFY8F9PIw=="],
|
||||
|
||||
"@codemirror/autocomplete": ["@codemirror/autocomplete@6.20.0", "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg=="],
|
||||
|
||||
"@codemirror/commands": ["@codemirror/commands@6.10.2", "https://registry.npmmirror.com/@codemirror/commands/-/commands-6.10.2.tgz", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ=="],
|
||||
|
||||
"@codemirror/lang-yaml": ["@codemirror/lang-yaml@6.1.2", "https://registry.npmmirror.com/@codemirror/lang-yaml/-/lang-yaml-6.1.2.tgz", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.2.0", "@lezer/lr": "^1.0.0", "@lezer/yaml": "^1.0.0" } }, "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw=="],
|
||||
|
||||
"@codemirror/language": ["@codemirror/language@6.12.2", "https://registry.npmmirror.com/@codemirror/language/-/language-6.12.2.tgz", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg=="],
|
||||
|
||||
"@codemirror/lint": ["@codemirror/lint@6.9.4", "https://registry.npmmirror.com/@codemirror/lint/-/lint-6.9.4.tgz", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.35.0", "crelt": "^1.0.5" } }, "sha512-ABc9vJ8DEmvOWuH26P3i8FpMWPQkduD9Rvba5iwb6O3hxASgclm3T3krGo8NASXkHCidz6b++LWlzWIUfEPSWw=="],
|
||||
|
||||
"@codemirror/search": ["@codemirror/search@6.6.0", "https://registry.npmmirror.com/@codemirror/search/-/search-6.6.0.tgz", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.37.0", "crelt": "^1.0.5" } }, "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw=="],
|
||||
|
||||
"@codemirror/state": ["@codemirror/state@6.5.4", "https://registry.npmmirror.com/@codemirror/state/-/state-6.5.4.tgz", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw=="],
|
||||
|
||||
"@codemirror/theme-one-dark": ["@codemirror/theme-one-dark@6.1.3", "https://registry.npmmirror.com/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/highlight": "^1.0.0" } }, "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA=="],
|
||||
|
||||
"@codemirror/view": ["@codemirror/view@6.39.15", "https://registry.npmmirror.com/@codemirror/view/-/view-6.39.15.tgz", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-aCWjgweIIXLBHh7bY6cACvXuyrZ0xGafjQ2VInjp4RM4gMfscK5uESiNdrH0pE+e1lZr2B4ONGsjchl2KsKZzg=="],
|
||||
|
||||
"@emnapi/core": ["@emnapi/core@1.8.1", "https://registry.npmmirror.com/@emnapi/core/-/core-1.8.1.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.8.1.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.3.tgz", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.3.tgz", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
|
||||
|
||||
"@floating-ui/core": ["@floating-ui/core@1.7.5", "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.5.tgz", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="],
|
||||
|
||||
"@floating-ui/dom": ["@floating-ui/dom@1.7.6", "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.6.tgz", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="],
|
||||
|
||||
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "https://registry.npmmirror.com/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="],
|
||||
|
||||
"@floating-ui/utils": ["@floating-ui/utils@0.2.11", "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.11.tgz", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", { "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", "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@kevisual/api": ["@kevisual/api@0.0.64", "https://registry.npmmirror.com/@kevisual/api/-/api-0.0.64.tgz", { "dependencies": { "@kevisual/context": "^0.0.8", "@kevisual/js-filter": "^0.0.6", "@kevisual/load": "^0.0.6", "@paralleldrive/cuid2": "^3.3.0", "es-toolkit": "^1.45.1", "eventemitter3": "^5.0.4", "fuse.js": "^7.1.0", "nanoid": "^5.1.6", "path-browserify-esm": "^1.0.6", "sonner": "^2.0.7", "spark-md5": "^3.0.2", "zustand": "^5.0.11" } }, "sha512-y7wP8ucvi/rflVGd6uJpvuEUTwI7wMef8+ITQzv4flg7a2pwWZYe/DT0TOyaqDAqKOTlXaVIdBeI15jXuUxIIg=="],
|
||||
|
||||
"@kevisual/cnb": ["@kevisual/cnb@0.0.45", "https://registry.npmmirror.com/@kevisual/cnb/-/cnb-0.0.45.tgz", { "dependencies": { "@kevisual/query": "^0.0.53", "@kevisual/router": "^0.1.1", "@kevisual/use-config": "^1.0.30", "@opencode-ai/sdk": "^1.2.24", "es-toolkit": "^1.45.1", "nanoid": "^5.1.6", "unstorage": "^1.17.4", "ws": "npm:@kevisual/ws", "zod": "^4.3.6" }, "bin": { "cnb": "bin/index.js", "cloud": "bin/index.js" } }, "sha512-+3eTtHNkf5BynLaY8oBIkIS4RzKcIDNVAyk1PF5oq1uHWRIEVgK7KwfgacrMlhUL03zrvO5wglB2uiUobjQyBQ=="],
|
||||
|
||||
"@kevisual/cnb-ai": ["@kevisual/cnb-ai@0.0.2", "https://registry.npmmirror.com/@kevisual/cnb-ai/-/cnb-ai-0.0.2.tgz", { "dependencies": { "@kevisual/cnb": "^0.0.24", "@kevisual/context": "^0.0.4", "@kevisual/query": "^0.0.40", "@kevisual/router": "^0.0.70", "ws": "npm:@kevisual/ws" } }, "sha512-GlyiYapf/3x4mPglhddJgZsS/LGY6/MOtYdmsPjKZVOdEEiRUrO0HRhIm1Xg0xuUGerj5B+orjIud8SvKbciQg=="],
|
||||
|
||||
"@kevisual/context": ["@kevisual/context@0.0.8", "https://registry.npmmirror.com/@kevisual/context/-/context-0.0.8.tgz", {}, "sha512-DTJpyHI34NE76B7g6f+QlIqiCCyqI2qkBMQE736dzeRDGxOjnbe2iQY9W+Rt2PE6kmymM3qyOmSfNovyWyWrkA=="],
|
||||
|
||||
"@kevisual/gitea": ["@kevisual/gitea@0.0.6", "https://registry.npmmirror.com/@kevisual/gitea/-/gitea-0.0.6.tgz", {}, "sha512-/qJha6IQ5VO+8WOGkLIMROmP0CvAaID0rPPyd5gtzl6yATqQLJS13Fm6Lfp5U5ImjTNmsq08khZqrj93Mz60cw=="],
|
||||
|
||||
"@kevisual/js-filter": ["@kevisual/js-filter@0.0.6", "https://registry.npmmirror.com/@kevisual/js-filter/-/js-filter-0.0.6.tgz", {}, "sha512-FcbOsmS1inhwrfgXMM/XLFTGTHUxBCss32JEMYdEFWQDYCar5rN8cxD1W8FuKDTVRlpA+zBpQ/BE6XT4UaeljA=="],
|
||||
|
||||
"@kevisual/kv-login": ["@kevisual/kv-login@0.1.18", "https://registry.npmmirror.com/@kevisual/kv-login/-/kv-login-0.1.18.tgz", {}, "sha512-voLPbG4Q/uPYazwMHx7bN9tf1Cs3amPU8ZcTrPukcLsvYQJZHqu9tB4Bi3/xAm6ZhBDoZ5VIQCwL7HuFCBsOMg=="],
|
||||
|
||||
"@kevisual/load": ["@kevisual/load@0.0.6", "https://registry.npmmirror.com/@kevisual/load/-/load-0.0.6.tgz", { "dependencies": { "eventemitter3": "^5.0.1" } }, "sha512-+3YTFehRcZ1haGel5DKYMUwmi5i6f2psyaPZlfkKU/cOXgkpwoG9/BEqPCnPjicKqqnksEpixVRkyHJ+5bjLVA=="],
|
||||
|
||||
"@kevisual/query": ["@kevisual/query@0.0.53", "https://registry.npmmirror.com/@kevisual/query/-/query-0.0.53.tgz", {}, "sha512-PAhpCLBr0emz0lGNlTVHMbJiC5wrtGLbInPddRzgKE35fiyNt+SWSsUWABiD0DeNrLN/OxWyAFobt880Z/e5MQ=="],
|
||||
|
||||
"@kevisual/router": ["@kevisual/router@0.1.1", "https://registry.npmmirror.com/@kevisual/router/-/router-0.1.1.tgz", { "dependencies": { "es-toolkit": "^1.45.1" } }, "sha512-+uaJc+Bf/T1mfxyfy9PmwuxJGPOLhVqrmsli2xUPqkkFvizrFIGB1vBTITuo5XP/FnwGqxgbjsitG57AMubm3w=="],
|
||||
|
||||
"@kevisual/types": ["@kevisual/types@0.0.12", "https://registry.npmmirror.com/@kevisual/types/-/types-0.0.12.tgz", {}, "sha512-zJXH2dosir3jVrQ6QG4i0+iLQeT9gJ3H+cKXs8ReWboxBSYzUZO78XssVeVrFPsJ33iaAqo4q3DWbSS1dWGn7Q=="],
|
||||
|
||||
"@kevisual/use-config": ["@kevisual/use-config@1.0.30", "https://registry.npmmirror.com/@kevisual/use-config/-/use-config-1.0.30.tgz", { "dependencies": { "@kevisual/load": "^0.0.6" }, "peerDependencies": { "dotenv": "^17" } }, "sha512-kPdna0FW/X7D600aMdiZ5UTjbCo6d8d4jjauSc8RMmBwUU6WliFDSPUNKVpzm2BsDX5Nth1IXFPYMqH+wxqAmw=="],
|
||||
|
||||
"@lezer/common": ["@lezer/common@1.5.1", "https://registry.npmmirror.com/@lezer/common/-/common-1.5.1.tgz", {}, "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw=="],
|
||||
|
||||
"@lezer/highlight": ["@lezer/highlight@1.2.3", "https://registry.npmmirror.com/@lezer/highlight/-/highlight-1.2.3.tgz", { "dependencies": { "@lezer/common": "^1.3.0" } }, "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g=="],
|
||||
|
||||
"@lezer/lr": ["@lezer/lr@1.4.8", "https://registry.npmmirror.com/@lezer/lr/-/lr-1.4.8.tgz", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA=="],
|
||||
|
||||
"@lezer/yaml": ["@lezer/yaml@1.0.4", "https://registry.npmmirror.com/@lezer/yaml/-/yaml-1.0.4.tgz", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.4.0" } }, "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw=="],
|
||||
|
||||
"@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "https://registry.npmmirror.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="],
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
|
||||
|
||||
"@noble/hashes": ["@noble/hashes@2.0.1", "https://registry.npmmirror.com/@noble/hashes/-/hashes-2.0.1.tgz", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="],
|
||||
|
||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.2.27", "https://registry.npmmirror.com/@opencode-ai/sdk/-/sdk-1.2.27.tgz", {}, "sha512-Wk0o/I+Fo+wE3zgvlJDs8Fb67KlKqX0PrV8dK5adSDkANq6r4Z25zXJg2iOir+a8ntg3rAcpel1OY4FV/TwRUA=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "https://registry.npmmirror.com/@opentelemetry/api/-/api-1.9.0.tgz", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@oxc-project/runtime": ["@oxc-project/runtime@0.115.0", "https://registry.npmmirror.com/@oxc-project/runtime/-/runtime-0.115.0.tgz", {}, "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.115.0", "https://registry.npmmirror.com/@oxc-project/types/-/types-0.115.0.tgz", {}, "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw=="],
|
||||
|
||||
"@paralleldrive/cuid2": ["@paralleldrive/cuid2@3.3.0", "https://registry.npmmirror.com/@paralleldrive/cuid2/-/cuid2-3.3.0.tgz", { "dependencies": { "@noble/hashes": "^2.0.1", "bignumber.js": "^9.3.1", "error-causes": "^3.0.2" }, "bin": { "cuid2": "bin/cuid2.js" } }, "sha512-OqiFvSOF0dBSesELYY2CAMa4YINvlLpvKOz/rv6NeZEqiyttlHgv98Juwv4Ch+GrEV7IZ8jfI2VcEoYUjXXCjw=="],
|
||||
|
||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.3.tgz", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
||||
|
||||
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", { "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", "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.2.tgz", { "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", "https://registry.npmmirror.com/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", { "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", "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", { "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", "https://registry.npmmirror.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", { "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", "https://registry.npmmirror.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", { "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", "https://registry.npmmirror.com/@radix-ui/react-id/-/react-id-1.1.1.tgz", { "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", "https://registry.npmmirror.com/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", { "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", "https://registry.npmmirror.com/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", { "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.4", "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "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-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
||||
|
||||
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", { "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-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", { "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", "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", { "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", "https://registry.npmmirror.com/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", { "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", "https://registry.npmmirror.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", { "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", "https://registry.npmmirror.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", { "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-rc.9", "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz", { "os": "android", "cpu": "arm64" }, "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg=="],
|
||||
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ=="],
|
||||
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg=="],
|
||||
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q=="],
|
||||
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz", { "os": "linux", "cpu": "arm" }, "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg=="],
|
||||
|
||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w=="],
|
||||
|
||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz", { "os": "linux", "cpu": "x64" }, "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz", { "os": "linux", "cpu": "x64" }, "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA=="],
|
||||
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz", { "os": "none", "cpu": "arm64" }, "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g=="],
|
||||
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA=="],
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz", { "os": "win32", "cpu": "x64" }, "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.2.1", "https://registry.npmmirror.com/@tailwindcss/node/-/node-4.2.1.tgz", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "https://registry.npmmirror.com/@tailwindcss/oxide/-/oxide-4.2.1.tgz", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="],
|
||||
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "https://registry.npmmirror.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="],
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "https://registry.npmmirror.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "https://registry.npmmirror.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "https://registry.npmmirror.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "https://registry.npmmirror.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="],
|
||||
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.2.1", "https://registry.npmmirror.com/@tailwindcss/vite/-/vite-4.2.1.tgz", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w=="],
|
||||
|
||||
"@tanstack/history": ["@tanstack/history@1.161.6", "https://registry.npmmirror.com/@tanstack/history/-/history-1.161.6.tgz", {}, "sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg=="],
|
||||
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.90.20", "https://registry.npmmirror.com/@tanstack/query-core/-/query-core-5.90.20.tgz", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="],
|
||||
|
||||
"@tanstack/react-query": ["@tanstack/react-query@5.90.21", "https://registry.npmmirror.com/@tanstack/react-query/-/react-query-5.90.21.tgz", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg=="],
|
||||
|
||||
"@tanstack/react-router": ["@tanstack/react-router@1.167.4", "https://registry.npmmirror.com/@tanstack/react-router/-/react-router-1.167.4.tgz", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.1", "@tanstack/router-core": "1.167.4", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-VpbZh382zX3WF4+X2Z+EUyd8eJhJyjg9C6ByYwrVZiWbhgbMK4+zQQIG2+lCAlIlDi7SV8fDcGL09NA8Z2kpGQ=="],
|
||||
|
||||
"@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.166.9", "https://registry.npmmirror.com/@tanstack/react-router-devtools/-/react-router-devtools-1.166.9.tgz", { "dependencies": { "@tanstack/router-devtools-core": "1.166.9" }, "peerDependencies": { "@tanstack/react-router": "^1.167.2", "@tanstack/router-core": "^1.167.2", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-O49eZmaeEKB5YnKH/qd61AbxV/lW8ICm4stfZ4GNQNpzQQ6rhPIB0p3PMZDIgX+6DoMivdNvLRmXAOOpzpIpDg=="],
|
||||
|
||||
"@tanstack/react-store": ["@tanstack/react-store@0.9.2", "https://registry.npmmirror.com/@tanstack/react-store/-/react-store-0.9.2.tgz", { "dependencies": { "@tanstack/store": "0.9.2", "use-sync-external-store": "^1.6.0" }, "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-Vt5usJE5sHG/cMechQfmwvwne6ktGCELe89Lmvoxe3LKRoFrhPa8OCKWs0NliG8HTJElEIj7PLtaBQIcux5pAQ=="],
|
||||
|
||||
"@tanstack/router-core": ["@tanstack/router-core@1.167.4", "https://registry.npmmirror.com/@tanstack/router-core/-/router-core-1.167.4.tgz", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/store": "^0.9.1", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "bin": { "intent": "bin/intent.js" } }, "sha512-Gk5V9Zr5JFJ4SbLyCheQLJ3MnXddccENPA+DJRz+9g3QxtN8DJB8w8KCUCgDeYlWp4LvmO4nX3fy3tupqVP2Pw=="],
|
||||
|
||||
"@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.166.9", "https://registry.npmmirror.com/@tanstack/router-devtools-core/-/router-devtools-core-1.166.9.tgz", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "@tanstack/router-core": "^1.167.2", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-PNlA7GmOUX9wY7LUG709Pk3Lg33dfHBztQwzjzrOiOsuf4ggp2R6bwarF8nYGNjG79z/MaB5PN+5yvkCVk8jGw=="],
|
||||
|
||||
"@tanstack/router-generator": ["@tanstack/router-generator@1.166.12", "https://registry.npmmirror.com/@tanstack/router-generator/-/router-generator-1.166.12.tgz", { "dependencies": { "@tanstack/router-core": "1.167.4", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.7", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-2HdxSTbCkbU9JeYogKVigIlXoLtIJE1x5rbEov+ZLTPjGCO9kicNQuljqg9Js+u2/ahtWewNrE5u1QCAyxmpIg=="],
|
||||
|
||||
"@tanstack/router-plugin": ["@tanstack/router-plugin@1.166.13", "https://registry.npmmirror.com/@tanstack/router-plugin/-/router-plugin-1.166.13.tgz", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.167.4", "@tanstack/router-generator": "1.166.12", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.7", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.167.4", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"], "bin": { "intent": "bin/intent.js" } }, "sha512-xG3ND3AlMe6DN9PihJAYUbQJevqJvVdzN1QpZbfU1/jkHurL97ynP2yXfmMTh8Qgi1K+SWRko4bi7iZlYP9SUw=="],
|
||||
|
||||
"@tanstack/router-utils": ["@tanstack/router-utils@1.161.6", "https://registry.npmmirror.com/@tanstack/router-utils/-/router-utils-1.161.6.tgz", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-nRcYw+w2OEgK6VfjirYvGyPLOK+tZQz1jkYcmH5AjMamQ9PycnlxZF2aEZtPpNoUsaceX2bHptn6Ub5hGXqNvw=="],
|
||||
|
||||
"@tanstack/store": ["@tanstack/store@0.9.2", "https://registry.npmmirror.com/@tanstack/store/-/store-0.9.2.tgz", {}, "sha512-K013lUJEFJK2ofFQ/hZKJUmCnpcV00ebLyOyFOWQvyQHUOZp/iYO84BM6aOGiV81JzwbX0APTVmW8YI7yiG5oA=="],
|
||||
|
||||
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.161.7", "https://registry.npmmirror.com/@tanstack/virtual-file-routes/-/virtual-file-routes-1.161.7.tgz", { "bin": { "intent": "bin/intent.js" } }, "sha512-olW33+Cn+bsCsZKPwEGhlkqS6w3M2slFv11JIobdnCFKMLG97oAI2kWKdx5/zsywTL8flpnoIgaZZPlQTFYhdQ=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
"@types/node": ["@types/node@25.5.0", "https://registry.npmmirror.com/@types/node/-/node-25.5.0.tgz", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.14", "https://registry.npmmirror.com/@types/react/-/react-19.2.14.tgz", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.2.3", "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.2.3.tgz", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||
|
||||
"@uiw/codemirror-extensions-basic-setup": ["@uiw/codemirror-extensions-basic-setup@4.25.8", "https://registry.npmmirror.com/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.8.tgz", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-9Rr+liiBmK4xzZHszL+twNRJApthqmITBwDP3emNTtTrkBFN4gHlqfp+nodKmoVt1+bUH1qQCtyqt+7dbDTHiw=="],
|
||||
|
||||
"@uiw/react-codemirror": ["@uiw/react-codemirror@4.25.8", "https://registry.npmmirror.com/@uiw/react-codemirror/-/react-codemirror-4.25.8.tgz", { "dependencies": { "@babel/runtime": "^7.18.6", "@codemirror/commands": "^6.1.0", "@codemirror/state": "^6.1.1", "@codemirror/theme-one-dark": "^6.0.0", "@uiw/codemirror-extensions-basic-setup": "4.25.8", "codemirror": "^6.0.0" }, "peerDependencies": { "@codemirror/view": ">=6.0.0", "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-A0aLOuJZm2yJ+U9GlMFwxwFciztjd5LhcAG4SMqFxdD58wH+sCQXuY4UU5J2hqgS390qAlShtUgREvJPUonbuQ=="],
|
||||
|
||||
"@vercel/oidc": ["@vercel/oidc@3.1.0", "https://registry.npmmirror.com/@vercel/oidc/-/oidc-3.1.0.tgz", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
"ai": ["ai@6.0.116", "https://registry.npmmirror.com/ai/-/ai-6.0.116.tgz", { "dependencies": { "@ai-sdk/gateway": "3.0.66", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7yM+cTmyRLeNIXwt4Vj+mrrJgVQ9RMIW5WO0ydoLoYkewIvsMcvUmqS4j2RJTUXaF1HphwmSKUMQ/HypNRGOmA=="],
|
||||
|
||||
"ansis": ["ansis@4.2.0", "https://registry.npmmirror.com/ansis/-/ansis-4.2.0.tgz", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="],
|
||||
|
||||
"anymatch": ["anymatch@3.1.3", "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
||||
|
||||
"aria-hidden": ["aria-hidden@1.2.6", "https://registry.npmmirror.com/aria-hidden/-/aria-hidden-1.2.6.tgz", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
|
||||
|
||||
"ast-types": ["ast-types@0.16.1", "https://registry.npmmirror.com/ast-types/-/ast-types-0.16.1.tgz", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
|
||||
|
||||
"babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "https://registry.npmmirror.com/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.12.tgz", { "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=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="],
|
||||
|
||||
"bignumber.js": ["bignumber.js@9.3.1", "https://registry.npmmirror.com/bignumber.js/-/bignumber.js-9.3.1.tgz", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
|
||||
|
||||
"binary-extensions": ["binary-extensions@2.3.0", "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"browserslist": ["browserslist@4.28.1", "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.1.tgz", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001770", "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", {}, "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw=="],
|
||||
|
||||
"chokidar": ["chokidar@3.6.0", "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", { "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", "https://registry.npmmirror.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"cmdk": ["cmdk@1.1.1", "https://registry.npmmirror.com/cmdk/-/cmdk-1.1.1.tgz", { "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=="],
|
||||
|
||||
"codemirror": ["codemirror@6.0.2", "https://registry.npmmirror.com/codemirror/-/codemirror-6.0.2.tgz", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"cookie-es": ["cookie-es@2.0.0", "https://registry.npmmirror.com/cookie-es/-/cookie-es-2.0.0.tgz", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="],
|
||||
|
||||
"crelt": ["crelt@1.0.6", "https://registry.npmmirror.com/crelt/-/crelt-1.0.6.tgz", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="],
|
||||
|
||||
"crossws": ["crossws@0.3.5", "https://registry.npmmirror.com/crossws/-/crossws-0.3.5.tgz", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="],
|
||||
|
||||
"csstype": ["csstype@3.2.3", "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"dayjs": ["dayjs@1.11.20", "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.20.tgz", {}, "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"defu": ["defu@6.1.4", "https://registry.npmmirror.com/defu/-/defu-6.1.4.tgz", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
||||
|
||||
"destr": ["destr@2.0.5", "https://registry.npmmirror.com/destr/-/destr-2.0.5.tgz", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"detect-node-es": ["detect-node-es@1.1.0", "https://registry.npmmirror.com/detect-node-es/-/detect-node-es-1.1.0.tgz", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
||||
|
||||
"diff": ["diff@8.0.3", "https://registry.npmmirror.com/diff/-/diff-8.0.3.tgz", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
|
||||
|
||||
"dotenv": ["dotenv@17.3.1", "https://registry.npmmirror.com/dotenv/-/dotenv-17.3.1.tgz", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.286", "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", {}, "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.19.0", "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="],
|
||||
|
||||
"error-causes": ["error-causes@3.0.2", "https://registry.npmmirror.com/error-causes/-/error-causes-3.0.2.tgz", {}, "sha512-i0B8zq1dHL6mM85FGoxaJnVtx6LD5nL2v0hlpGdntg5FOSyzQ46c9lmz5qx0xRS2+PWHGOHcYxGIBC5Le2dRMw=="],
|
||||
|
||||
"es-toolkit": ["es-toolkit@1.45.1", "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.45.1.tgz", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="],
|
||||
|
||||
"esbuild": ["esbuild@0.27.3", "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.3.tgz", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"esprima": ["esprima@4.0.1", "https://registry.npmmirror.com/esprima/-/esprima-4.0.1.tgz", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
|
||||
|
||||
"eventemitter3": ["eventemitter3@5.0.4", "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.4.tgz", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
|
||||
|
||||
"eventsource-parser": ["eventsource-parser@3.0.6", "https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-3.0.6.tgz", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fill-range": ["fill-range@7.1.1", "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"fuse.js": ["fuse.js@7.1.0", "https://registry.npmmirror.com/fuse.js/-/fuse.js-7.1.0.tgz", {}, "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ=="],
|
||||
|
||||
"gensync": ["gensync@1.0.0-beta.2", "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
|
||||
"get-nonce": ["get-nonce@1.0.1", "https://registry.npmmirror.com/get-nonce/-/get-nonce-1.0.1.tgz", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
||||
|
||||
"get-tsconfig": ["get-tsconfig@4.13.6", "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.13.6.tgz", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="],
|
||||
|
||||
"glob-parent": ["glob-parent@5.1.2", "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"goober": ["goober@2.1.18", "https://registry.npmmirror.com/goober/-/goober-2.1.18.tgz", { "peerDependencies": { "csstype": "^3.0.10" } }, "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"h3": ["h3@1.15.5", "https://registry.npmmirror.com/h3/-/h3-1.15.5.tgz", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.5", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg=="],
|
||||
|
||||
"idb-keyval": ["idb-keyval@6.2.2", "https://registry.npmmirror.com/idb-keyval/-/idb-keyval-6.2.2.tgz", {}, "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg=="],
|
||||
|
||||
"iron-webcrypto": ["iron-webcrypto@1.2.1", "https://registry.npmmirror.com/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="],
|
||||
|
||||
"is-binary-path": ["is-binary-path@2.1.0", "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"is-number": ["is-number@7.0.0", "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||
|
||||
"isbot": ["isbot@5.1.35", "https://registry.npmmirror.com/isbot/-/isbot-5.1.35.tgz", {}, "sha512-waFfC72ZNfwLLuJ2iLaoVaqcNo+CAaLR7xCpAn0Y5WfGzkNHv7ZN39Vbi1y+kb+Zs46XHOX3tZNExroFUPX+Kg=="],
|
||||
|
||||
"jiti": ["jiti@2.6.1", "https://registry.npmmirror.com/jiti/-/jiti-2.6.1.tgz", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
"json-schema": ["json-schema@0.4.0", "https://registry.npmmirror.com/json-schema/-/json-schema-0.4.0.tgz", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
||||
|
||||
"json5": ["json5@2.2.3", "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.32.0", "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
||||
|
||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
||||
|
||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
||||
|
||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
||||
|
||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
||||
|
||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
||||
|
||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
||||
|
||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
||||
|
||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||
|
||||
"lru-cache": ["lru-cache@11.2.6", "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.2.6.tgz", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
|
||||
|
||||
"lucide-react": ["lucide-react@0.577.0", "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.577.0.tgz", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@5.1.6", "https://registry.npmmirror.com/nanoid/-/nanoid-5.1.6.tgz", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
|
||||
|
||||
"next-themes": ["next-themes@0.4.6", "https://registry.npmmirror.com/next-themes/-/next-themes-0.4.6.tgz", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
|
||||
|
||||
"node-fetch-native": ["node-fetch-native@1.6.7", "https://registry.npmmirror.com/node-fetch-native/-/node-fetch-native-1.6.7.tgz", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
||||
|
||||
"node-mock-http": ["node-mock-http@1.0.4", "https://registry.npmmirror.com/node-mock-http/-/node-mock-http-1.0.4.tgz", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.27", "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.27.tgz", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
|
||||
|
||||
"normalize-path": ["normalize-path@3.0.0", "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||
|
||||
"ofetch": ["ofetch@1.5.1", "https://registry.npmmirror.com/ofetch/-/ofetch-1.5.1.tgz", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
|
||||
|
||||
"path-browserify-esm": ["path-browserify-esm@1.0.6", "https://registry.npmmirror.com/path-browserify-esm/-/path-browserify-esm-1.0.6.tgz", {}, "sha512-9nUwYvvu/yq1PYrUyYCihNWmpzacaRYF6gGbjLWErrZ4MRDWyfPN7RpE8E7tsw8eqBU/rr7mcoTXbS+Vih8uUA=="],
|
||||
|
||||
"pathe": ["pathe@2.0.3", "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"postcss": ["postcss@8.5.8", "https://registry.npmmirror.com/postcss/-/postcss-8.5.8.tgz", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
||||
|
||||
"prettier": ["prettier@3.8.1", "https://registry.npmmirror.com/prettier/-/prettier-3.8.1.tgz", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
|
||||
|
||||
"radix3": ["radix3@1.1.2", "https://registry.npmmirror.com/radix3/-/radix3-1.1.2.tgz", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
|
||||
|
||||
"react": ["react@19.2.4", "https://registry.npmmirror.com/react/-/react-19.2.4.tgz", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
|
||||
|
||||
"react-dom": ["react-dom@19.2.4", "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.4.tgz", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
|
||||
|
||||
"react-hook-form": ["react-hook-form@7.71.2", "https://registry.npmmirror.com/react-hook-form/-/react-hook-form-7.71.2.tgz", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA=="],
|
||||
|
||||
"react-remove-scroll": ["react-remove-scroll@2.7.2", "https://registry.npmmirror.com/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", { "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", "https://registry.npmmirror.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", { "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", "https://registry.npmmirror.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz", { "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", "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||
|
||||
"recast": ["recast@0.23.11", "https://registry.npmmirror.com/recast/-/recast-0.23.11.tgz", { "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=="],
|
||||
|
||||
"reselect": ["reselect@5.1.1", "https://registry.npmmirror.com/reselect/-/reselect-5.1.1.tgz", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="],
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
|
||||
"rolldown": ["rolldown@1.0.0-rc.9", "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.0-rc.9.tgz", { "dependencies": { "@oxc-project/types": "=0.115.0", "@rolldown/pluginutils": "1.0.0-rc.9" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.9", "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", "@rolldown/binding-darwin-x64": "1.0.0-rc.9", "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q=="],
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"semver": ["semver@6.3.1", "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"seroval": ["seroval@1.5.0", "https://registry.npmmirror.com/seroval/-/seroval-1.5.0.tgz", {}, "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw=="],
|
||||
|
||||
"seroval-plugins": ["seroval-plugins@1.5.0", "https://registry.npmmirror.com/seroval-plugins/-/seroval-plugins-1.5.0.tgz", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA=="],
|
||||
|
||||
"sonner": ["sonner@2.0.7", "https://registry.npmmirror.com/sonner/-/sonner-2.0.7.tgz", { "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-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
|
||||
|
||||
"source-map": ["source-map@0.7.6", "https://registry.npmmirror.com/source-map/-/source-map-0.7.6.tgz", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"spark-md5": ["spark-md5@3.0.2", "https://registry.npmmirror.com/spark-md5/-/spark-md5-3.0.2.tgz", {}, "sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw=="],
|
||||
|
||||
"style-mod": ["style-mod@4.1.3", "https://registry.npmmirror.com/style-mod/-/style-mod-4.1.3.tgz", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
|
||||
|
||||
"tabbable": ["tabbable@6.4.0", "https://registry.npmmirror.com/tabbable/-/tabbable-6.4.0.tgz", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="],
|
||||
|
||||
"tailwind-merge": ["tailwind-merge@3.5.0", "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-3.5.0.tgz", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.2.1", "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.2.1.tgz", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
|
||||
|
||||
"tapable": ["tapable@2.3.0", "https://registry.npmmirror.com/tapable/-/tapable-2.3.0.tgz", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||
|
||||
"tiny-invariant": ["tiny-invariant@1.3.3", "https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||
|
||||
"tiny-warning": ["tiny-warning@1.0.3", "https://registry.npmmirror.com/tiny-warning/-/tiny-warning-1.0.3.tgz", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"tsx": ["tsx@4.21.0", "https://registry.npmmirror.com/tsx/-/tsx-4.21.0.tgz", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="],
|
||||
|
||||
"tw-animate-css": ["tw-animate-css@1.4.0", "https://registry.npmmirror.com/tw-animate-css/-/tw-animate-css-1.4.0.tgz", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"ufo": ["ufo@1.6.3", "https://registry.npmmirror.com/ufo/-/ufo-1.6.3.tgz", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
|
||||
|
||||
"uncrypto": ["uncrypto@0.1.3", "https://registry.npmmirror.com/uncrypto/-/uncrypto-0.1.3.tgz", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
|
||||
|
||||
"undici-types": ["undici-types@7.18.2", "https://registry.npmmirror.com/undici-types/-/undici-types-7.18.2.tgz", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
"unplugin": ["unplugin@2.3.11", "https://registry.npmmirror.com/unplugin/-/unplugin-2.3.11.tgz", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="],
|
||||
|
||||
"unstorage": ["unstorage@1.17.4", "https://registry.npmmirror.com/unstorage/-/unstorage-1.17.4.tgz", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.5", "lru-cache": "^11.2.0", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-fHK0yNg38tBiJKp/Vgsq4j0JEsCmgqH58HAn707S7zGkArbZsVr/CwINoi+nh3h98BRCwKvx1K3Xg9u3VV83sw=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.2.3", "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", { "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", "https://registry.npmmirror.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz", { "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", "https://registry.npmmirror.com/use-sidecar/-/use-sidecar-1.1.3.tgz", { "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", "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
||||
|
||||
"vite": ["vite@8.0.0", "https://registry.npmmirror.com/vite/-/vite-8.0.0.tgz", { "dependencies": { "@oxc-project/runtime": "0.115.0", "lightningcss": "^1.32.0", "picomatch": "^4.0.3", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.9", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.0.0-alpha.31", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q=="],
|
||||
|
||||
"w3c-keyname": ["w3c-keyname@2.2.8", "https://registry.npmmirror.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
|
||||
|
||||
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
|
||||
|
||||
"ws": ["@kevisual/ws@8.19.0", "https://registry.npmmirror.com/@kevisual/ws/-/ws-8.19.0.tgz", {}, "sha512-jLsL80wBBKkrJZrfk3SQpJ9JA/zREdlUROj7eCkmzqduAWKSI0wVcXuCKf+mLFCHB0Q0Tkh2rgzjSlurt3JQgw=="],
|
||||
|
||||
"yallist": ["yallist@3.1.1", "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"zod": ["zod@4.3.6", "https://registry.npmmirror.com/zod/-/zod-4.3.6.tgz", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||
|
||||
"zustand": ["zustand@5.0.11", "https://registry.npmmirror.com/zustand/-/zustand-5.0.11.tgz", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg=="],
|
||||
|
||||
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"@kevisual/cnb-ai/@kevisual/cnb": ["@kevisual/cnb@0.0.24", "https://registry.npmmirror.com/@kevisual/cnb/-/cnb-0.0.24.tgz", { "dependencies": { "@kevisual/query": "^0.0.40", "@kevisual/router": "^0.0.70", "@kevisual/use-config": "^1.0.30", "es-toolkit": "^1.44.0", "nanoid": "^5.1.6", "unstorage": "^1.17.4", "ws": "npm:@kevisual/ws", "zod": "^4.3.6" } }, "sha512-LxFhnf7hAyXlLn+CJihKeNud9wwo2QBj5QQY1eQCeDFlujnSGyc9WkqKG4dHTe9wdTDLbxanlb5/BXzFHudTbw=="],
|
||||
|
||||
"@kevisual/cnb-ai/@kevisual/context": ["@kevisual/context@0.0.4", "https://registry.npmmirror.com/@kevisual/context/-/context-0.0.4.tgz", {}, "sha512-HJeLeZQLU+7tCluSfOyvkgKLs0HjCZrdJlZgEgKRSa8XTwZfMAUt6J7qZTbrZAHBlPtX68EPu/PI8JMCeu3WAQ=="],
|
||||
|
||||
"@kevisual/cnb-ai/@kevisual/query": ["@kevisual/query@0.0.40", "https://registry.npmmirror.com/@kevisual/query/-/query-0.0.40.tgz", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-7m5BgDzd01m51hCHUId6ugQHdwgrLTb6fI7DSuMY17VjWb0+zGnkYmvRBqkTXzoIjjYbP5iwtRnrooEoToQfhg=="],
|
||||
|
||||
"@kevisual/cnb-ai/@kevisual/router": ["@kevisual/router@0.0.70", "https://registry.npmmirror.com/@kevisual/router/-/router-0.0.70.tgz", { "dependencies": { "es-toolkit": "^1.44.0" } }, "sha512-vXlIj9jRufhcIfeuPWemjSI+dxdzSmIBq5eRxQzqEfAJ7k+mBPhoI4KxH8vHnwyL30bqm8EdODL/p6Wg8uBw3g=="],
|
||||
|
||||
"@radix-ui/react-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", { "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-primitive": ["@radix-ui/react-primitive@2.1.3", "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", { "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-primitive": ["@radix-ui/react-primitive@2.1.3", "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", { "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-primitive": ["@radix-ui/react-primitive@2.1.3", "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", { "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-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", { "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=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss": ["lightningcss@1.31.1", "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.31.1.tgz", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "https://registry.npmmirror.com/@emnapi/core/-/core-1.8.1.tgz", { "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", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.8.1.tgz", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", { "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.1", "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@tanstack/router-generator/zod": ["zod@3.25.76", "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"@tanstack/router-plugin/zod": ["zod@3.25.76", "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"anymatch/picomatch": ["picomatch@2.3.1", "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"h3/cookie-es": ["cookie-es@1.2.2", "https://registry.npmmirror.com/cookie-es/-/cookie-es-1.2.2.tgz", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="],
|
||||
|
||||
"postcss/nanoid": ["nanoid@3.3.11", "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"readdirp/picomatch": ["picomatch@2.3.1", "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"recast/source-map": ["source-map@0.6.1", "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz", {}, "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw=="],
|
||||
|
||||
"unstorage/chokidar": ["chokidar@5.0.0", "https://registry.npmmirror.com/chokidar/-/chokidar-5.0.0.tgz", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||
|
||||
"@kevisual/cnb-ai/@kevisual/cnb/es-toolkit": ["es-toolkit@1.44.0", "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.44.0.tgz", {}, "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg=="],
|
||||
|
||||
"@kevisual/cnb-ai/@kevisual/router/es-toolkit": ["es-toolkit@1.44.0", "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.44.0.tgz", {}, "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
|
||||
|
||||
"unstorage/chokidar/readdirp": ["readdirp@5.0.0", "https://registry.npmmirror.com/readdirp/-/readdirp-5.0.0.tgz", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"style": "base-nova",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="https://kevisual.xiongxiao.me/root/center/panda.png" />
|
||||
<link rel="icon" type="image/jpg" href="https://kevisual.cn/root/logo/assets/logos/cnb-gateway-logo-980x980.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
<link rel="stylesheet" href="/src/index.css" />
|
||||
<title>CNB Center</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
|
||||
21
kevisual.json
Normal file
21
kevisual.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "kevisual",
|
||||
"share": "public"
|
||||
},
|
||||
"registry": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template",
|
||||
"clone": {
|
||||
".": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"syncd": [
|
||||
{
|
||||
"files": [
|
||||
"**/*"
|
||||
],
|
||||
"registry": ""
|
||||
}
|
||||
],
|
||||
"sync": {}
|
||||
}
|
||||
74
package.json
74
package.json
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "vite-react",
|
||||
"name": "@kevisual/cnb-center",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.8",
|
||||
"type": "module",
|
||||
"basename": "/",
|
||||
"basename": "/root/cnb-center",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"ui": "pnpm dlx shadcn@latest add ",
|
||||
"pub": "envision deploy ./dist -k vite-react -v 0.0.1 -y y -u"
|
||||
"pub": "envision deploy ./dist -k cnb-center -v 0.0.8 -y y -u"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
@@ -17,39 +17,61 @@
|
||||
"author": "abearxiong <xiongxiao@xiongxiao.me>",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@kevisual/router": "0.0.70",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@tanstack/react-router": "^1.158.0",
|
||||
"@ai-sdk/anthropic": "^3.0.58",
|
||||
"@ai-sdk/openai": "^3.0.41",
|
||||
"@ai-sdk/openai-compatible": "^2.0.35",
|
||||
"@base-ui/react": "^1.3.0",
|
||||
"@kevisual/api": "^0.0.64",
|
||||
"@kevisual/cnb": "^0.0.52",
|
||||
"@kevisual/cnb-ai": "^0.0.2",
|
||||
"@kevisual/context": "^0.0.8",
|
||||
"@kevisual/kv-login": "^0.1.18",
|
||||
"@kevisual/router": "0.1.3",
|
||||
"@tanstack/react-router": "^1.167.4",
|
||||
"@uiw/react-codemirror": "^4.25.8",
|
||||
"ai": "^6.0.116",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.19",
|
||||
"es-toolkit": "^1.44.0",
|
||||
"lucide-react": "^0.563.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"cmdk": "^1.1.1",
|
||||
"dayjs": "^1.11.20",
|
||||
"es-toolkit": "^1.45.1",
|
||||
"fuse.js": "^7.1.0",
|
||||
"idb-keyval": "^6.2.2",
|
||||
"lucide-react": "^0.577.0",
|
||||
"nanoid": "^5.1.7",
|
||||
"next-themes": "^0.4.6",
|
||||
"re-resizable": "^6.11.2",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-hook-form": "^7.71.1",
|
||||
"zustand": "^5.0.11"
|
||||
"react-hook-form": "^7.71.2",
|
||||
"react-resizable": "^3.1.3",
|
||||
"sonner": "^2.0.7",
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
"zod": "^4.3.6",
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kevisual/query": "0.0.39",
|
||||
"@codemirror/lang-yaml": "^6.1.2",
|
||||
"@kevisual/gitea": "^0.0.6",
|
||||
"@kevisual/query": "0.0.53",
|
||||
"@kevisual/types": "^0.0.12",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/react-router-devtools": "^1.158.0",
|
||||
"@tanstack/router-plugin": "^1.158.0",
|
||||
"@types/node": "^25.2.0",
|
||||
"@types/react": "^19.2.10",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"@tanstack/react-router-devtools": "^1.166.9",
|
||||
"@tanstack/router-plugin": "^1.166.13",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.3",
|
||||
"dotenv": "^17.2.3",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"@types/react-resizable": "^3.0.8",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"dotenv": "^17.3.1",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1"
|
||||
},
|
||||
"packageManager": "pnpm@10.28.2"
|
||||
"vite": "^8.0.0"
|
||||
}
|
||||
}
|
||||
3645
pnpm-lock.yaml
generated
3645
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
44
public/auth.json
Normal file
44
public/auth.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "kevisual",
|
||||
"share": "public"
|
||||
},
|
||||
"registry": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template",
|
||||
"clone": {
|
||||
".": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"syncd": [
|
||||
{
|
||||
"files": [
|
||||
"**/*"
|
||||
],
|
||||
"registry": ""
|
||||
}
|
||||
],
|
||||
"scripts": {
|
||||
"auth": "ev sync clone -l -i https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/public/auth.json"
|
||||
},
|
||||
"sync": {
|
||||
"AGENTS.md": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/AGENTS.md",
|
||||
"vite.config.ts": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/vite.config.ts",
|
||||
"src/main.tsx": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/main.tsx",
|
||||
"public/auth.json": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/public/auth.json",
|
||||
"src/agents/index.ts": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/agents/index.ts",
|
||||
"src/modules/basename.ts": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/modules/basename.ts",
|
||||
"src/modules/query.ts": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/modules/query.ts",
|
||||
"src/routes/demo.tsx": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/routes/demo.tsx",
|
||||
"src/routes/index.tsx": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/routes/index.tsx",
|
||||
"src/routes/login.tsx": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/routes/login.tsx",
|
||||
"src/styles/theme.css": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/styles/theme.css",
|
||||
"src/pages/auth/index.tsx": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/pages/auth/index.tsx",
|
||||
"src/pages/auth/page.tsx": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/pages/auth/page.tsx",
|
||||
"src/pages/auth/store.ts": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/pages/auth/store.ts",
|
||||
"src/pages/demo/page.tsx": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/pages/demo/page.tsx",
|
||||
"src/pages/auth/hooks/index.ts": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/pages/auth/hooks/index.ts",
|
||||
"src/pages/auth/hooks/use-api-query.ts": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/pages/auth/hooks/use-api-query.ts",
|
||||
"src/pages/auth/modules/BaseHeader.tsx": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/pages/auth/modules/BaseHeader.tsx",
|
||||
"src/pages/demo/store/index.ts": "https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/src/pages/demo/store/index.ts"
|
||||
}
|
||||
}
|
||||
25
public/comments.html
Normal file
25
public/comments.html
Normal file
@@ -0,0 +1,25 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/png" href="https://kevisual.xiongxiao.me/root/center/panda.jpg" />
|
||||
<title>Comments</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<script
|
||||
src="https://commit.cool/client.js"
|
||||
repo="kevisual/kevisual"
|
||||
issue-term="custom"
|
||||
issue-id="comments"
|
||||
theme="light"
|
||||
layout="classic"
|
||||
crossorigin="anonymous"
|
||||
async
|
||||
></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
BIN
public/images/config-info.png
Normal file
BIN
public/images/config-info.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
BIN
public/images/repo-info.png
Normal file
BIN
public/images/repo-info.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
10
src/agents/app.ts
Normal file
10
src/agents/app.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { QueryRouterServer } from '@kevisual/router/browser'
|
||||
import { useContextKey } from '@kevisual/context'
|
||||
export const app = useContextKey('app', new QueryRouterServer())
|
||||
|
||||
// import '@kevisual/cnb-ai'
|
||||
|
||||
const url = 'https://kevisual.cn/root/cnb-ai/dist/app.js'
|
||||
setTimeout(() => {
|
||||
import(/* @vite-ignore */url)
|
||||
}, 2000)
|
||||
1
src/agents/index.ts
Normal file
1
src/agents/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './app.ts'
|
||||
281
src/components/a/Sidebar.tsx
Normal file
281
src/components/a/Sidebar.tsx
Normal file
@@ -0,0 +1,281 @@
|
||||
'use client'
|
||||
import { useNavigate, useLocation } from '@tanstack/react-router'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { ChevronLeft, ChevronRight, ChevronDown } from 'lucide-react'
|
||||
import { Resizable } from 're-resizable'
|
||||
|
||||
export interface NavItem {
|
||||
title: string
|
||||
path: string
|
||||
icon?: React.ReactNode
|
||||
isDeveloping?: boolean
|
||||
badge?: string
|
||||
hidden?: boolean
|
||||
children?: NavItem[]
|
||||
external?: boolean
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
export interface SidebarProps {
|
||||
items: NavItem[]
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
logo?: React.ReactNode
|
||||
title?: React.ReactNode
|
||||
defaultCollapsed?: boolean
|
||||
defaultWidth?: number
|
||||
minWidth?: number
|
||||
maxWidth?: number
|
||||
}
|
||||
|
||||
export function Sidebar({
|
||||
items,
|
||||
className,
|
||||
children,
|
||||
logo,
|
||||
title,
|
||||
defaultCollapsed = false,
|
||||
defaultWidth = 208,
|
||||
minWidth = 120,
|
||||
maxWidth = 400,
|
||||
}: SidebarProps) {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const currentPath = location.pathname
|
||||
|
||||
const [collapsed, setCollapsed] = useState(defaultCollapsed)
|
||||
const [sidebarWidth, setSidebarWidth] = useState(defaultWidth)
|
||||
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set())
|
||||
const [developingDialog, setDevelopingDialog] = useState<{ open: boolean; title: string }>({
|
||||
open: false,
|
||||
title: '',
|
||||
})
|
||||
|
||||
const toggleGroup = (path: string) => {
|
||||
const newExpanded = new Set(expandedGroups)
|
||||
if (newExpanded.has(path)) {
|
||||
newExpanded.delete(path)
|
||||
} else {
|
||||
newExpanded.add(path)
|
||||
}
|
||||
setExpandedGroups(newExpanded)
|
||||
}
|
||||
|
||||
const handleNavClick = (item: NavItem) => {
|
||||
// 优先执行 onClick 回调
|
||||
if (item.onClick) {
|
||||
item.onClick()
|
||||
return
|
||||
}
|
||||
|
||||
if (item.isDeveloping) {
|
||||
setDevelopingDialog({ open: true, title: item.title })
|
||||
} else if (item.external && item.path.startsWith('http')) {
|
||||
window.open(item.path, '_blank')
|
||||
} else if (item.path.startsWith('/')) {
|
||||
navigate({ to: item.path })
|
||||
} else {
|
||||
navigate({ href: item.path })
|
||||
}
|
||||
}
|
||||
|
||||
// 判断当前路径是否激活(以导航路径开头)
|
||||
const isActive = (path: string) => {
|
||||
if (path === '/') {
|
||||
return currentPath === '/'
|
||||
}
|
||||
return currentPath.startsWith(path);
|
||||
}
|
||||
|
||||
// 渲染导航项
|
||||
const renderNavItem = (item: NavItem, isChild = false) => {
|
||||
if (item.hidden) return null
|
||||
|
||||
const hasChildren = item.children && item.children.length > 0
|
||||
const isExpanded = expandedGroups.has(item.path)
|
||||
const active = isActive(item.path)
|
||||
|
||||
return (
|
||||
<li key={item.path} className='list-none'>
|
||||
{hasChildren ? (
|
||||
// 父菜单项(可展开)
|
||||
<div>
|
||||
<button
|
||||
onClick={() => toggleGroup(item.path)}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-3 px-3 py-2 text-sm rounded-lg transition-colors cursor-pointer',
|
||||
active
|
||||
? 'bg-gray-200 text-gray-900 font-medium'
|
||||
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900',
|
||||
collapsed && 'justify-center px-2'
|
||||
)}
|
||||
title={collapsed ? item.title : undefined}
|
||||
>
|
||||
{item.icon && <span className="flex-shrink-0">{item.icon}</span>}
|
||||
{!collapsed && (
|
||||
<>
|
||||
<span className="flex-1 text-left">{item.title}</span>
|
||||
<ChevronDown className={cn('w-4 h-4 transition-transform', isExpanded && 'rotate-180')} />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{/* 子菜单 */}
|
||||
{!collapsed && isExpanded && item.children && (
|
||||
<ul className="ml-6 mt-1 space-y-1 list-none">
|
||||
{item.children.map(child => renderNavItem(child, true))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// 普通菜单项
|
||||
<button
|
||||
onClick={() => handleNavClick(item)}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-3 px-3 py-2 text-sm rounded-lg transition-colors cursor-pointer',
|
||||
active
|
||||
? 'bg-gray-200 text-gray-900 font-medium'
|
||||
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900',
|
||||
item.isDeveloping && 'opacity-60',
|
||||
isChild && 'py-1.5',
|
||||
collapsed && 'justify-center px-2'
|
||||
)}
|
||||
title={collapsed ? item.title : undefined}
|
||||
>
|
||||
{item.icon && <span className="flex-shrink-0">{item.icon}</span>}
|
||||
{!collapsed && (
|
||||
<>
|
||||
<span className="flex-1 text-left">{item.title}</span>
|
||||
{item.isDeveloping && (
|
||||
<span className="text-xs bg-gray-200 text-gray-600 px-1.5 py-0.5 rounded">
|
||||
dev
|
||||
</span>
|
||||
)}
|
||||
{item.badge && !item.isDeveloping && (
|
||||
<span className="text-xs bg-gray-200 text-gray-700 px-1.5 py-0.5 rounded">
|
||||
{item.badge}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cn('flex h-full', className)}>
|
||||
{/* 侧边栏 */}
|
||||
{!collapsed ? (
|
||||
<Resizable
|
||||
defaultSize={{ width: sidebarWidth, height: '100%' }}
|
||||
minWidth={minWidth}
|
||||
maxWidth={maxWidth}
|
||||
onResizeStop={(_e, _direction, ref, _d) => {
|
||||
setSidebarWidth(ref.offsetWidth)
|
||||
}}
|
||||
enable={{
|
||||
right: true,
|
||||
}}
|
||||
handleComponent={{
|
||||
right: <div className="w-1 h-full cursor-col-resize hover:bg-blue-400 transition-colors" />,
|
||||
}}
|
||||
>
|
||||
<aside
|
||||
className={cn(
|
||||
'border-r bg-white flex-shrink-0 flex flex-col'
|
||||
)}
|
||||
style={{ width: sidebarWidth }}
|
||||
>
|
||||
{/* Logo 区域 */}
|
||||
<div className={cn(
|
||||
'h-12 flex items-center border-b px-3'
|
||||
)}>
|
||||
{logo && (
|
||||
<div className="flex-shrink-0 flex items-center gap-2">{logo}</div>
|
||||
)}
|
||||
{title && (
|
||||
<span className="text-sm font-medium text-gray-900 truncate ml-1">{title}</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setCollapsed(true)}
|
||||
className="ml-auto flex-shrink-0 p-1 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer"
|
||||
title="收起"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4 text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 导航区域 */}
|
||||
<nav className="flex-1 p-2 overflow-y-auto">
|
||||
<ul className="space-y-1 list-none">
|
||||
{items.map((item) => renderNavItem(item))}
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
</Resizable>
|
||||
) : (
|
||||
// 收起状态
|
||||
<aside className="w-14 border-r bg-white flex-shrink-0 flex flex-col">
|
||||
{/* Logo 区域 */}
|
||||
<div className="h-12 flex items-center justify-center border-b px-2">
|
||||
<div className="group relative flex-shrink-0">
|
||||
<button
|
||||
onClick={() => setCollapsed(false)}
|
||||
className="p-1 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer"
|
||||
title="展开"
|
||||
>
|
||||
<span className="group-hover:hidden">{logo}</span>
|
||||
<ChevronRight className="w-5 h-5 hidden group-hover:block text-gray-600" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 导航区域 */}
|
||||
<nav className="flex-1 p-2 overflow-y-auto">
|
||||
<ul className="space-y-1 list-none">
|
||||
{items.map((item) => renderNavItem(item))}
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
)}
|
||||
|
||||
{/* 主内容区域 */}
|
||||
<main className="flex-1 overflow-auto h-full bg-gray-50">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* 开发中弹窗 */}
|
||||
<Dialog
|
||||
open={developingDialog.open}
|
||||
onOpenChange={(open) => setDevelopingDialog({ open, title: '' })}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{developingDialog.title} - 开发中</DialogTitle>
|
||||
<DialogDescription>
|
||||
该功能正在紧张开发中,敬请期待!暂时无法访问。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => setDevelopingDialog({ open: false, title: '' })}>
|
||||
知道了
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
78
src/components/tags-input.tsx
Normal file
78
src/components/tags-input.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import * as React from "react"
|
||||
import { X } from "lucide-react"
|
||||
import { Badge } from "./ui/badge"
|
||||
import { Input } from "./ui/input"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface TagsInputProps {
|
||||
value: string[]
|
||||
onChange: (tags: string[]) => void
|
||||
placeholder?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function TagsInput({ value, onChange, placeholder, className }: TagsInputProps) {
|
||||
const [inputValue, setInputValue] = React.useState("")
|
||||
const inputRef = React.useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter" || e.key === ",") {
|
||||
e.preventDefault()
|
||||
addTag()
|
||||
} else if (e.key === "Backspace" && !inputValue && value.length > 0) {
|
||||
removeTag(value.length - 1)
|
||||
}
|
||||
}
|
||||
|
||||
const addTag = () => {
|
||||
const trimmedValue = inputValue.trim()
|
||||
if (trimmedValue && !value.includes(trimmedValue)) {
|
||||
onChange([...value, trimmedValue])
|
||||
setInputValue("")
|
||||
}
|
||||
}
|
||||
|
||||
const removeTag = (index: number) => {
|
||||
onChange(value.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full flex-wrap gap-2 rounded-md border border-neutral-200 bg-white px-3 py-3 text-sm ring-offset-white transition-all hover:border-neutral-300 focus-within:ring-2 focus-within:ring-neutral-950 focus-within:ring-offset-2 cursor-text",
|
||||
className
|
||||
)}
|
||||
onClick={() => inputRef.current?.focus()}
|
||||
>
|
||||
{value.map((tag, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
variant="outline"
|
||||
className="gap-1.5 pl-2.5 pr-1.5 py-1 border-neutral-300 text-neutral-700 bg-neutral-50 hover:bg-neutral-100 transition-colors h-7 font-normal"
|
||||
>
|
||||
<span className="text-xs">{tag}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
removeTag(index)
|
||||
}}
|
||||
className="ml-0.5 rounded-full hover:bg-neutral-200 hover:text-neutral-900 p-0.5 transition-colors"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
<Input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={addTag}
|
||||
placeholder={value.length === 0 ? placeholder : ""}
|
||||
className="flex-1 min-w-[150px] border-0 p-0 focus-visible:ring-0 focus-visible:ring-offset-0 h-7 placeholder:text-neutral-400"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
104
src/components/ui/avatar.tsx
Normal file
104
src/components/ui/avatar.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import * as React from "react"
|
||||
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: AvatarPrimitive.Root.Props & {
|
||||
size?: "default" | "sm" | "lg"
|
||||
}) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"size-8 rounded-full after:rounded-full data-[size=lg]:size-10 data-[size=sm]:size-6 after:border-border group/avatar relative flex shrink-0 select-none after:absolute after:inset-0 after:border after:mix-blend-darken dark:after:mix-blend-lighten",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn(
|
||||
"rounded-full aspect-square size-full object-cover",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: AvatarPrimitive.Fallback.Props) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"bg-muted text-muted-foreground rounded-full flex size-full items-center justify-center text-sm group-data-[size=sm]/avatar:text-xs",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="avatar-badge"
|
||||
className={cn(
|
||||
"bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-blend-color ring-2 select-none",
|
||||
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
|
||||
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
|
||||
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="avatar-group"
|
||||
className={cn(
|
||||
"*:data-[slot=avatar]:ring-background group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarGroupCount({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="avatar-group-count"
|
||||
className={cn("bg-muted text-muted-foreground size-8 rounded-full text-sm group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3 ring-background relative flex shrink-0 items-center justify-center ring-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Avatar,
|
||||
AvatarImage,
|
||||
AvatarFallback,
|
||||
AvatarGroup,
|
||||
AvatarGroupCount,
|
||||
AvatarBadge,
|
||||
}
|
||||
48
src/components/ui/badge.tsx
Normal file
48
src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { mergeProps } from "@base-ui/react/merge-props"
|
||||
import { useRender } from "@base-ui/react/use-render"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 [&>svg]:pointer-events-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 overflow-hidden group/badge",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
||||
destructive: "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20",
|
||||
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
||||
ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant = "default",
|
||||
render,
|
||||
...props
|
||||
}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
|
||||
return useRender({
|
||||
defaultTagName: "span",
|
||||
props: mergeProps<"span">(
|
||||
{
|
||||
className: cn(badgeVariants({ className, variant })),
|
||||
},
|
||||
props
|
||||
),
|
||||
render,
|
||||
state: {
|
||||
slot: "badge",
|
||||
variant,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
125
src/components/ui/breadcrumb.tsx
Normal file
125
src/components/ui/breadcrumb.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import * as React from "react"
|
||||
import { mergeProps } from "@base-ui/react/merge-props"
|
||||
import { useRender } from "@base-ui/react/use-render"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react"
|
||||
|
||||
function Breadcrumb({ className, ...props }: React.ComponentProps<"nav">) {
|
||||
return (
|
||||
<nav
|
||||
aria-label="breadcrumb"
|
||||
data-slot="breadcrumb"
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||
return (
|
||||
<ol
|
||||
data-slot="breadcrumb-list"
|
||||
className={cn(
|
||||
"text-muted-foreground gap-1.5 text-sm flex flex-wrap items-center wrap-break-word",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-item"
|
||||
className={cn("gap-1 inline-flex items-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbLink({
|
||||
className,
|
||||
render,
|
||||
...props
|
||||
}: useRender.ComponentProps<"a">) {
|
||||
return useRender({
|
||||
defaultTagName: "a",
|
||||
props: mergeProps<"a">(
|
||||
{
|
||||
className: cn("hover:text-foreground transition-colors", className),
|
||||
},
|
||||
props
|
||||
),
|
||||
render,
|
||||
state: {
|
||||
slot: "breadcrumb-link",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-page"
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn("text-foreground font-normal", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbSeparator({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-separator"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("[&>svg]:size-3.5", className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? (
|
||||
<ChevronRightIcon />
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbEllipsis({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-ellipsis"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"size-5 [&>svg]:size-4 flex items-center justify-center",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontalIcon
|
||||
/>
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
}
|
||||
@@ -1,30 +1,29 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { Button as ButtonPrimitive } from "@base-ui/react/button"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
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-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-lg border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 aria-invalid:ring-3 [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
outline: "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||
ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
|
||||
destructive: "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
default: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
||||
icon: "size-8",
|
||||
"icon-xs": "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-sm": "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
||||
"icon-lg": "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
@@ -34,24 +33,19 @@ const buttonVariants = cva(
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
function Button({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
...props
|
||||
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
|
||||
return (
|
||||
<ButtonPrimitive
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
|
||||
94
src/components/ui/card.tsx
Normal file
94
src/components/ui/card.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
data-size={size}
|
||||
className={cn("ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-xl py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("text-base leading-snug font-medium group-data-[size=sm]/card:text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("bg-muted/50 rounded-b-xl border-t p-4 group-data-[size=sm]/card:p-3 flex items-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
29
src/components/ui/checkbox.tsx
Normal file
29
src/components/ui/checkbox.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
|
||||
function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-[4px] border transition-colors group-has-disabled/field:opacity-50 focus-visible:ring-3 aria-invalid:ring-3 peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="[&>svg]:size-3.5 grid place-content-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon
|
||||
/>
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Checkbox }
|
||||
188
src/components/ui/command.tsx
Normal file
188
src/components/ui/command.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import * as React from "react"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
} from "@/components/ui/input-group"
|
||||
import { SearchIcon, CheckIcon } from "lucide-react"
|
||||
|
||||
function Command({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||
return (
|
||||
<CommandPrimitive
|
||||
data-slot="command"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground rounded-xl! p-1 flex size-full flex-col overflow-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandDialog({
|
||||
title = "Command Palette",
|
||||
description = "Search for a command to run...",
|
||||
children,
|
||||
className,
|
||||
showCloseButton = false,
|
||||
...props
|
||||
}: Omit<React.ComponentProps<typeof Dialog>, "children"> & {
|
||||
title?: string
|
||||
description?: string
|
||||
className?: string
|
||||
showCloseButton?: boolean
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogContent
|
||||
className={cn(
|
||||
"rounded-xl! top-1/3 translate-y-0 overflow-hidden p-0",
|
||||
className
|
||||
)}
|
||||
showCloseButton={showCloseButton}
|
||||
>
|
||||
{children}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||
return (
|
||||
<div data-slot="command-input-wrapper" className="p-1 pb-0">
|
||||
<InputGroup className="bg-input/30 border-input/30 h-8! rounded-lg! shadow-none! *:data-[slot=input-group-addon]:pl-2!">
|
||||
<CommandPrimitive.Input
|
||||
data-slot="command-input"
|
||||
className={cn(
|
||||
"w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<InputGroupAddon>
|
||||
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||
return (
|
||||
<CommandPrimitive.List
|
||||
data-slot="command-list"
|
||||
className={cn(
|
||||
"no-scrollbar max-h-72 scroll-py-1 outline-none overflow-x-hidden overflow-y-auto",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandEmpty({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||
return (
|
||||
<CommandPrimitive.Empty
|
||||
data-slot="command-empty"
|
||||
className={cn("py-6 text-center text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||
return (
|
||||
<CommandPrimitive.Group
|
||||
data-slot="command-group"
|
||||
className={cn("text-foreground **:[[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||
return (
|
||||
<CommandPrimitive.Separator
|
||||
data-slot="command-separator"
|
||||
className={cn("bg-border -mx-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||
return (
|
||||
<CommandPrimitive.Item
|
||||
data-slot="command-item"
|
||||
className={cn(
|
||||
"data-selected:bg-muted data-selected:text-foreground data-selected:*:[svg]:text-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none in-data-[slot=dialog-content]:rounded-lg! [&_svg:not([class*='size-'])]:size-4 group/command-item data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<CheckIcon className="ml-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100" />
|
||||
</CommandPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="command-shortcut"
|
||||
className={cn("text-muted-foreground group-data-selected/command-item:text-foreground ml-auto text-xs tracking-widest", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
}
|
||||
149
src/components/ui/dialog.tsx
Normal file
149
src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import * as React from "react"
|
||||
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
function Dialog({ ...props }: DialogPrimitive.Root.Props) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: DialogPrimitive.Backdrop.Props) {
|
||||
return (
|
||||
<DialogPrimitive.Backdrop
|
||||
data-slot="dialog-overlay"
|
||||
className={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: DialogPrimitive.Popup.Props & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Popup
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-4 rounded-xl p-4 text-sm ring-1 duration-100 sm:max-w-sm fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="absolute top-2 right-2"
|
||||
size="icon-sm"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<XIcon
|
||||
/>
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Popup>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("gap-2 flex flex-col", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({
|
||||
className,
|
||||
showCloseButton = false,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"bg-muted/50 -mx-4 -mb-4 rounded-b-xl border-t p-4 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close render={<Button variant="outline" />}>
|
||||
Close
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-base leading-none font-medium", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: DialogPrimitive.Description.Props) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
260
src/components/ui/dropdown-menu.tsx
Normal file
260
src/components/ui/dropdown-menu.tsx
Normal file
@@ -0,0 +1,260 @@
|
||||
import * as React from "react"
|
||||
import { Menu as MenuPrimitive } from "@base-ui/react/menu"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ChevronRightIcon, CheckIcon } from "lucide-react"
|
||||
|
||||
function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
|
||||
return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) {
|
||||
return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {
|
||||
return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
align = "start",
|
||||
alignOffset = 0,
|
||||
side = "bottom",
|
||||
sideOffset = 4,
|
||||
className,
|
||||
...props
|
||||
}: MenuPrimitive.Popup.Props &
|
||||
Pick<
|
||||
MenuPrimitive.Positioner.Props,
|
||||
"align" | "alignOffset" | "side" | "sideOffset"
|
||||
>) {
|
||||
return (
|
||||
<MenuPrimitive.Portal>
|
||||
<MenuPrimitive.Positioner
|
||||
className="isolate z-50 outline-none"
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
>
|
||||
<MenuPrimitive.Popup
|
||||
data-slot="dropdown-menu-content"
|
||||
className={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-lg p-1 shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 max-h-(--available-height) w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto outline-none data-closed:overflow-hidden", className )}
|
||||
{...props}
|
||||
/>
|
||||
</MenuPrimitive.Positioner>
|
||||
</MenuPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) {
|
||||
return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: MenuPrimitive.GroupLabel.Props & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<MenuPrimitive.GroupLabel
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn("text-muted-foreground px-1.5 py-1 text-xs font-medium data-inset:pl-7", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: MenuPrimitive.Item.Props & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
}) {
|
||||
return (
|
||||
<MenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md px-1.5 py-1 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 group/dropdown-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {
|
||||
return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: MenuPrimitive.SubmenuTrigger.Props & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<MenuPrimitive.SubmenuTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md px-1.5 py-1 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 data-popup-open:bg-accent data-popup-open:text-accent-foreground flex cursor-default items-center outline-hidden select-none [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto" />
|
||||
</MenuPrimitive.SubmenuTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
align = "start",
|
||||
alignOffset = -3,
|
||||
side = "right",
|
||||
sideOffset = 0,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuContent>) {
|
||||
return (
|
||||
<DropdownMenuContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/10 bg-popover text-popover-foreground min-w-[96px] rounded-lg p-1 shadow-lg ring-1 duration-100 w-auto", className)}
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
inset,
|
||||
...props
|
||||
}: MenuPrimitive.CheckboxItem.Props & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<MenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
className="absolute right-2 flex items-center justify-center pointer-events-none"
|
||||
data-slot="dropdown-menu-checkbox-item-indicator"
|
||||
>
|
||||
<MenuPrimitive.CheckboxItemIndicator>
|
||||
<CheckIcon
|
||||
/>
|
||||
</MenuPrimitive.CheckboxItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenuPrimitive.CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {
|
||||
return (
|
||||
<MenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
inset,
|
||||
...props
|
||||
}: MenuPrimitive.RadioItem.Props & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<MenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
className="absolute right-2 flex items-center justify-center pointer-events-none"
|
||||
data-slot="dropdown-menu-radio-item-indicator"
|
||||
>
|
||||
<MenuPrimitive.RadioItemIndicator>
|
||||
<CheckIcon
|
||||
/>
|
||||
</MenuPrimitive.RadioItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenuPrimitive.RadioItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: MenuPrimitive.Separator.Props) {
|
||||
return (
|
||||
<MenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn("text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
}
|
||||
149
src/components/ui/input-group.tsx
Normal file
149
src/components/ui/input-group.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
|
||||
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="input-group"
|
||||
role="group"
|
||||
className={cn(
|
||||
"border-input dark:bg-input/30 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-disabled:bg-input/50 dark:has-disabled:bg-input/80 h-8 rounded-lg border transition-colors in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-3 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5 group/input-group relative flex w-full min-w-0 items-center outline-none has-[>textarea]:h-auto",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const inputGroupAddonVariants = cva(
|
||||
"text-muted-foreground h-auto gap-2 py-1.5 text-sm font-medium group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4 flex cursor-text items-center justify-center select-none",
|
||||
{
|
||||
variants: {
|
||||
align: {
|
||||
"inline-start": "pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem] order-first",
|
||||
"inline-end": "pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem] order-last",
|
||||
"block-start":
|
||||
"px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2 order-first w-full justify-start",
|
||||
"block-end":
|
||||
"px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2 order-last w-full justify-start",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
align: "inline-start",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function InputGroupAddon({
|
||||
className,
|
||||
align = "inline-start",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
data-slot="input-group-addon"
|
||||
data-align={align}
|
||||
className={cn(inputGroupAddonVariants({ align }), className)}
|
||||
onClick={(e) => {
|
||||
if ((e.target as HTMLElement).closest("button")) {
|
||||
return
|
||||
}
|
||||
e.currentTarget.parentElement?.querySelector("input")?.focus()
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const inputGroupButtonVariants = cva(
|
||||
"gap-2 text-sm shadow-none flex items-center",
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
|
||||
sm: "",
|
||||
"icon-xs": "size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0",
|
||||
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "xs",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function InputGroupButton({
|
||||
className,
|
||||
type = "button",
|
||||
variant = "ghost",
|
||||
size = "xs",
|
||||
...props
|
||||
}: Omit<React.ComponentProps<typeof Button>, "size" | "type"> &
|
||||
VariantProps<typeof inputGroupButtonVariants> & {
|
||||
type?: "button" | "submit" | "reset"
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
type={type}
|
||||
data-size={size}
|
||||
variant={variant}
|
||||
className={cn(inputGroupButtonVariants({ size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"text-muted-foreground gap-2 text-sm [&_svg:not([class*='size-'])]:size-4 flex items-center [&_svg]:pointer-events-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function InputGroupInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<Input
|
||||
data-slot="input-group-control"
|
||||
className={cn("rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent flex-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function InputGroupTextarea({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<Textarea
|
||||
data-slot="input-group-control"
|
||||
className={cn("rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent flex-1 resize-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupText,
|
||||
InputGroupInput,
|
||||
InputGroupTextarea,
|
||||
}
|
||||
20
src/components/ui/input.tsx
Normal file
20
src/components/ui/input.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import * as React from "react"
|
||||
import { Input as InputPrimitive } from "@base-ui/react/input"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<InputPrimitive
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors file:h-6 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
26
src/components/ui/kbd.tsx
Normal file
26
src/components/ui/kbd.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
|
||||
return (
|
||||
<kbd
|
||||
data-slot="kbd"
|
||||
className={cn(
|
||||
"bg-muted text-muted-foreground in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10 h-5 w-fit min-w-5 gap-1 rounded-sm px-1 font-sans text-xs font-medium [&_svg:not([class*='size-'])]:size-3 pointer-events-none inline-flex items-center justify-center select-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<kbd
|
||||
data-slot="kbd-group"
|
||||
className={cn("gap-1 inline-flex items-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Kbd, KbdGroup }
|
||||
18
src/components/ui/label.tsx
Normal file
18
src/components/ui/label.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Label({ className, ...props }: React.ComponentProps<"label">) {
|
||||
return (
|
||||
<label
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"gap-2 text-sm leading-none font-medium group-data-[disabled=true]:opacity-50 peer-disabled:opacity-50 flex items-center select-none group-data-[disabled=true]:pointer-events-none peer-disabled:cursor-not-allowed",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
||||
265
src/components/ui/menubar.tsx
Normal file
265
src/components/ui/menubar.tsx
Normal file
@@ -0,0 +1,265 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Menu as MenuPrimitive } from "@base-ui/react/menu"
|
||||
import { Menubar as MenubarPrimitive } from "@base-ui/react/menubar"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
|
||||
function Menubar({ className, ...props }: MenubarPrimitive.Props) {
|
||||
return (
|
||||
<MenubarPrimitive
|
||||
data-slot="menubar"
|
||||
className={cn("bg-background h-8 gap-0.5 rounded-lg border p-[3px] flex items-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarMenu({ ...props }: React.ComponentProps<typeof DropdownMenu>) {
|
||||
return <DropdownMenu data-slot="menubar-menu" {...props} />
|
||||
}
|
||||
|
||||
function MenubarGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuGroup>) {
|
||||
return <DropdownMenuGroup data-slot="menubar-group" {...props} />
|
||||
}
|
||||
|
||||
function MenubarPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPortal>) {
|
||||
return <DropdownMenuPortal data-slot="menubar-portal" {...props} />
|
||||
}
|
||||
|
||||
function MenubarTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuTrigger>) {
|
||||
return (
|
||||
<DropdownMenuTrigger
|
||||
data-slot="menubar-trigger"
|
||||
className={cn(
|
||||
"hover:bg-muted aria-expanded:bg-muted rounded-sm px-1.5 py-[2px] text-sm font-medium flex items-center outline-hidden select-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarContent({
|
||||
className,
|
||||
align = "start",
|
||||
alignOffset = -4,
|
||||
sideOffset = 8,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuContent>) {
|
||||
return (
|
||||
<DropdownMenuContent
|
||||
data-slot="menubar-content"
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
sideOffset={sideOffset}
|
||||
className={cn("bg-popover text-popover-foreground data-open:animate-in data-open:fade-in-0 data-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 ring-foreground/10 min-w-36 rounded-lg p-1 shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2", className )}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuItem>) {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
data-slot="menubar-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn("focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive! not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md px-1.5 py-1 text-sm data-disabled:opacity-50 data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 group/menubar-item", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
inset,
|
||||
...props
|
||||
}: MenuPrimitive.CheckboxItem.Props & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<MenuPrimitive.CheckboxItem
|
||||
data-slot="menubar-checkbox-item"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-1.5 rounded-md py-1 pr-1.5 pl-7 text-sm data-inset:pl-7 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="left-1.5 size-4 [&_svg:not([class*='size-'])]:size-4 pointer-events-none absolute flex items-center justify-center">
|
||||
<MenuPrimitive.CheckboxItemIndicator>
|
||||
<CheckIcon
|
||||
/>
|
||||
</MenuPrimitive.CheckboxItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenuPrimitive.CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuRadioGroup>) {
|
||||
return <DropdownMenuRadioGroup data-slot="menubar-radio-group" {...props} />
|
||||
}
|
||||
|
||||
function MenubarRadioItem({
|
||||
className,
|
||||
children,
|
||||
inset,
|
||||
...props
|
||||
}: MenuPrimitive.RadioItem.Props & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<MenuPrimitive.RadioItem
|
||||
data-slot="menubar-radio-item"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-1.5 rounded-md py-1 pr-1.5 pl-7 text-sm data-disabled:opacity-50 data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="left-1.5 size-4 [&_svg:not([class*='size-'])]:size-4 pointer-events-none absolute flex items-center justify-center">
|
||||
<MenuPrimitive.RadioItemIndicator>
|
||||
<CheckIcon
|
||||
/>
|
||||
</MenuPrimitive.RadioItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenuPrimitive.RadioItem>
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuLabel> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuLabel
|
||||
data-slot="menubar-label"
|
||||
data-inset={inset}
|
||||
className={cn("px-1.5 py-1 text-sm font-medium data-inset:pl-7", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuSeparator>) {
|
||||
return (
|
||||
<DropdownMenuSeparator
|
||||
data-slot="menubar-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuShortcut>) {
|
||||
return (
|
||||
<DropdownMenuShortcut
|
||||
data-slot="menubar-shortcut"
|
||||
className={cn("text-muted-foreground group-focus/menubar-item:text-accent-foreground text-xs tracking-widest ml-auto", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuSub>) {
|
||||
return <DropdownMenuSub data-slot="menubar-sub" {...props} />
|
||||
}
|
||||
|
||||
function MenubarSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuSubTrigger> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuSubTrigger
|
||||
data-slot="menubar-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn("focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground gap-1.5 rounded-md px-1.5 py-1 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuSubContent>) {
|
||||
return (
|
||||
<DropdownMenuSubContent
|
||||
data-slot="menubar-sub-content"
|
||||
className={cn("bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/10 min-w-32 rounded-lg p-1 shadow-lg ring-1 duration-100", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Menubar,
|
||||
MenubarPortal,
|
||||
MenubarMenu,
|
||||
MenubarTrigger,
|
||||
MenubarContent,
|
||||
MenubarGroup,
|
||||
MenubarSeparator,
|
||||
MenubarLabel,
|
||||
MenubarItem,
|
||||
MenubarShortcut,
|
||||
MenubarCheckboxItem,
|
||||
MenubarRadioGroup,
|
||||
MenubarRadioItem,
|
||||
MenubarSub,
|
||||
MenubarSubTrigger,
|
||||
MenubarSubContent,
|
||||
}
|
||||
88
src/components/ui/popover.tsx
Normal file
88
src/components/ui/popover.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import * as React from "react"
|
||||
import { Popover as PopoverPrimitive } from "@base-ui/react/popover"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Popover({ ...props }: PopoverPrimitive.Root.Props) {
|
||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||
}
|
||||
|
||||
function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props) {
|
||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||
}
|
||||
|
||||
function PopoverContent({
|
||||
className,
|
||||
align = "center",
|
||||
alignOffset = 0,
|
||||
side = "bottom",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: PopoverPrimitive.Popup.Props &
|
||||
Pick<
|
||||
PopoverPrimitive.Positioner.Props,
|
||||
"align" | "alignOffset" | "side" | "sideOffset"
|
||||
>) {
|
||||
return (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Positioner
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
className="isolate z-50"
|
||||
>
|
||||
<PopoverPrimitive.Popup
|
||||
data-slot="popover-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/10 flex flex-col gap-2.5 rounded-lg p-2.5 text-sm shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 w-72 origin-(--transform-origin) outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Positioner>
|
||||
</PopoverPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="popover-header"
|
||||
className={cn("flex flex-col gap-0.5 text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) {
|
||||
return (
|
||||
<PopoverPrimitive.Title
|
||||
data-slot="popover-title"
|
||||
className={cn("font-medium", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PopoverDescription({
|
||||
className,
|
||||
...props
|
||||
}: PopoverPrimitive.Description.Props) {
|
||||
return (
|
||||
<PopoverPrimitive.Description
|
||||
data-slot="popover-description"
|
||||
className={cn("text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverDescription,
|
||||
PopoverHeader,
|
||||
PopoverTitle,
|
||||
PopoverTrigger,
|
||||
}
|
||||
191
src/components/ui/select.tsx
Normal file
191
src/components/ui/select.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import * as React from "react"
|
||||
import { Select as SelectPrimitive } from "@base-ui/react/select"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
|
||||
return (
|
||||
<SelectPrimitive.Group
|
||||
data-slot="select-group"
|
||||
className={cn("scroll-my-1 p-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
|
||||
return (
|
||||
<SelectPrimitive.Value
|
||||
data-slot="select-value"
|
||||
className={cn("flex flex-1 text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: SelectPrimitive.Trigger.Props & {
|
||||
size?: "sm" | "default"
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-placeholder:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 gap-1.5 rounded-lg border bg-transparent py-2 pr-2 pl-2.5 text-sm transition-colors select-none focus-visible:ring-3 aria-invalid:ring-3 data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:gap-1.5 [&_svg:not([class*='size-'])]:size-4 flex w-fit items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon
|
||||
render={
|
||||
<ChevronDownIcon className="text-muted-foreground size-4 pointer-events-none" />
|
||||
}
|
||||
/>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
side = "bottom",
|
||||
sideOffset = 4,
|
||||
align = "center",
|
||||
alignOffset = 0,
|
||||
alignItemWithTrigger = true,
|
||||
...props
|
||||
}: SelectPrimitive.Popup.Props &
|
||||
Pick<
|
||||
SelectPrimitive.Positioner.Props,
|
||||
"align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger"
|
||||
>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Positioner
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
alignItemWithTrigger={alignItemWithTrigger}
|
||||
className="isolate z-50"
|
||||
>
|
||||
<SelectPrimitive.Popup
|
||||
data-slot="select-content"
|
||||
data-align-trigger={alignItemWithTrigger}
|
||||
className={cn("bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/10 min-w-36 rounded-lg shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 relative isolate z-50 max-h-(--available-height) w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto data-[align-trigger=true]:animate-none", className )}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.List>{children}</SelectPrimitive.List>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Popup>
|
||||
</SelectPrimitive.Positioner>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: SelectPrimitive.GroupLabel.Props) {
|
||||
return (
|
||||
<SelectPrimitive.GroupLabel
|
||||
data-slot="select-label"
|
||||
className={cn("text-muted-foreground px-1.5 py-1 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: SelectPrimitive.Item.Props) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.ItemText className="flex flex-1 gap-2 shrink-0 whitespace-nowrap">
|
||||
{children}
|
||||
</SelectPrimitive.ItemText>
|
||||
<SelectPrimitive.ItemIndicator
|
||||
render={<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />}
|
||||
>
|
||||
<CheckIcon className="pointer-events-none" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: SelectPrimitive.Separator.Props) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px pointer-events-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpArrow
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn("bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4 top-0 w-full", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon
|
||||
/>
|
||||
</SelectPrimitive.ScrollUpArrow>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownArrow
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn("bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4 bottom-0 w-full", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon
|
||||
/>
|
||||
</SelectPrimitive.ScrollDownArrow>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
||||
25
src/components/ui/separator.tsx
Normal file
25
src/components/ui/separator.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
"use client"
|
||||
|
||||
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
...props
|
||||
}: SeparatorPrimitive.Props) {
|
||||
return (
|
||||
<SeparatorPrimitive
|
||||
data-slot="separator"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-border shrink-0 data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Separator }
|
||||
129
src/components/ui/sheet.tsx
Normal file
129
src/components/ui/sheet.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Dialog as SheetPrimitive } from "@base-ui/react/dialog"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
function Sheet({ ...props }: SheetPrimitive.Root.Props) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||
}
|
||||
|
||||
function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) {
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||
}
|
||||
|
||||
function SheetClose({ ...props }: SheetPrimitive.Close.Props) {
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||
}
|
||||
|
||||
function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) {
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||
}
|
||||
|
||||
function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
|
||||
return (
|
||||
<SheetPrimitive.Backdrop
|
||||
data-slot="sheet-overlay"
|
||||
className={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetContent({
|
||||
className,
|
||||
children,
|
||||
side = "right",
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: SheetPrimitive.Popup.Props & {
|
||||
side?: "top" | "right" | "bottom" | "left"
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Popup
|
||||
data-slot="sheet-content"
|
||||
data-side={side}
|
||||
className={cn("bg-background data-open:animate-in data-closed:animate-out data-[side=right]:data-closed:slide-out-to-right-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=top]:data-closed:slide-out-to-top-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:fade-out-0 data-open:fade-in-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=bottom]:data-open:slide-in-from-bottom-10 fixed z-50 flex flex-col gap-4 bg-clip-padding text-sm shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<SheetPrimitive.Close
|
||||
data-slot="sheet-close"
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="absolute top-3 right-3"
|
||||
size="icon-sm"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<XIcon
|
||||
/>
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
)}
|
||||
</SheetPrimitive.Popup>
|
||||
</SheetPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-header"
|
||||
className={cn("gap-0.5 p-4 flex flex-col", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-footer"
|
||||
className={cn("gap-2 p-4 mt-auto flex flex-col", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
className={cn("text-foreground text-base font-medium", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetDescription({
|
||||
className,
|
||||
...props
|
||||
}: SheetPrimitive.Description.Props) {
|
||||
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,
|
||||
}
|
||||
13
src/components/ui/skeleton.tsx
Normal file
13
src/components/ui/skeleton.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="skeleton"
|
||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
49
src/components/ui/sonner.tsx
Normal file
49
src/components/ui/sonner.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
"use client"
|
||||
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
||||
import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react"
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
icons={{
|
||||
success: (
|
||||
<CircleCheckIcon className="size-4" />
|
||||
),
|
||||
info: (
|
||||
<InfoIcon className="size-4" />
|
||||
),
|
||||
warning: (
|
||||
<TriangleAlertIcon className="size-4" />
|
||||
),
|
||||
error: (
|
||||
<OctagonXIcon className="size-4" />
|
||||
),
|
||||
loading: (
|
||||
<Loader2Icon className="size-4 animate-spin" />
|
||||
),
|
||||
}}
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
"--border-radius": "var(--radius)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast: "cn-toast",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
||||
101
src/components/ui/table.tsx
Normal file
101
src/components/ui/table.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
return (
|
||||
<div data-slot="table-container" className="relative w-full overflow-x-auto">
|
||||
<table
|
||||
data-slot="table"
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn("[&_tr]:border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||
return (
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={cn("bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn("hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn("text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn("p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCaption({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"caption">) {
|
||||
return (
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
80
src/components/ui/tabs.tsx
Normal file
80
src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
...props
|
||||
}: TabsPrimitive.Root.Props) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
data-orientation={orientation}
|
||||
className={cn(
|
||||
"gap-2 group/tabs flex data-horizontal:flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const tabsListVariants = cva(
|
||||
"rounded-lg p-[3px] group-data-horizontal/tabs:h-8 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-muted",
|
||||
line: "gap-1 bg-transparent",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
variant = "default",
|
||||
...props
|
||||
}: TabsPrimitive.List.Props & VariantProps<typeof tabsListVariants>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
data-variant={variant}
|
||||
className={cn(tabsListVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
|
||||
return (
|
||||
<TabsPrimitive.Tab
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg:not([class*='size-'])]:size-4 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center whitespace-nowrap transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
|
||||
"data-active:bg-background dark:data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 data-active:text-foreground",
|
||||
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {
|
||||
return (
|
||||
<TabsPrimitive.Panel
|
||||
data-slot="tabs-content"
|
||||
className={cn("text-sm flex-1 outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
|
||||
18
src/components/ui/textarea.tsx
Normal file
18
src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"border-input dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 rounded-lg border bg-transparent px-2.5 py-2 text-base transition-colors focus-visible:ring-3 aria-invalid:ring-3 md:text-sm placeholder:text-muted-foreground flex field-sizing-content min-h-16 w-full outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Textarea }
|
||||
64
src/components/ui/tooltip.tsx
Normal file
64
src/components/ui/tooltip.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function TooltipProvider({
|
||||
delay = 0,
|
||||
...props
|
||||
}: TooltipPrimitive.Provider.Props) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delay={delay}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
|
||||
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
}
|
||||
|
||||
function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
side = "top",
|
||||
sideOffset = 4,
|
||||
align = "center",
|
||||
alignOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: TooltipPrimitive.Popup.Props &
|
||||
Pick<
|
||||
TooltipPrimitive.Positioner.Props,
|
||||
"align" | "alignOffset" | "side" | "sideOffset"
|
||||
>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Positioner
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
className="isolate z-50"
|
||||
>
|
||||
<TooltipPrimitive.Popup
|
||||
data-slot="tooltip-content"
|
||||
className={cn(
|
||||
"data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-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 rounded-md px-3 py-1.5 text-xs data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 bg-foreground text-background z-50 w-fit max-w-xs origin-(--transform-origin)",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 bg-foreground fill-foreground z-50 data-[side=bottom]:top-1 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
|
||||
</TooltipPrimitive.Popup>
|
||||
</TooltipPrimitive.Positioner>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
@@ -2,12 +2,12 @@ import ReactDOM from 'react-dom/client'
|
||||
import { RouterProvider, createRouter } from '@tanstack/react-router'
|
||||
import { routeTree } from './routeTree.gen'
|
||||
import './index.css'
|
||||
import { basename } from './modules/basename'
|
||||
|
||||
import { getDynamicBasename } from './modules/basename'
|
||||
import './agents/index.ts';
|
||||
// Set up a Router instance
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
basepath: basename,
|
||||
basepath: getDynamicBasename(),
|
||||
defaultPreload: 'intent',
|
||||
scrollRestoration: true,
|
||||
})
|
||||
|
||||
@@ -1,2 +1,32 @@
|
||||
// @ts-ignore
|
||||
export const basename = BASE_NAME;
|
||||
|
||||
export const wrapBasename = (path: string) => {
|
||||
const hasEnd = path.endsWith('/')
|
||||
if (basename) {
|
||||
return `${basename}${path}` + (hasEnd ? '' : '/');
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
// 动态计算 basename,根据当前 URL 路径
|
||||
export const getDynamicBasename = (): string => {
|
||||
const path = window.location.pathname
|
||||
const [user, key, id] = path.split('/').filter(Boolean)
|
||||
if (key === 'v1' && id) {
|
||||
return `/${user}/v1/${id}`
|
||||
}
|
||||
// 默认使用构建时的 basename
|
||||
return basename
|
||||
}
|
||||
|
||||
export const openLink = (path: string, target: string = '_self') => {
|
||||
if (path.startsWith('http://') || path.startsWith('https://')) {
|
||||
window.open(path, target);
|
||||
return;
|
||||
}
|
||||
const url = new URL(path, window.location.origin);
|
||||
url.pathname = wrapBasename(url.pathname);
|
||||
window.open(url.toString(), target);
|
||||
}
|
||||
1227
src/modules/cnb-api.ts
Normal file
1227
src/modules/cnb-api.ts
Normal file
File diff suppressed because it is too large
Load Diff
378
src/modules/mark-api.ts
Normal file
378
src/modules/mark-api.ts
Normal file
@@ -0,0 +1,378 @@
|
||||
import { createQueryApi } from '@kevisual/query/api';
|
||||
const api = {
|
||||
"mark": {
|
||||
/**
|
||||
* 获取mark列表
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.page - {number} 页码
|
||||
* @param data.pageSize - {number} 每页数量
|
||||
* @param data.search - {string} 搜索关键词
|
||||
* @param data.markType - {string} mark类型,simple,wallnote,md,draw等
|
||||
* @param data.sort - {"DESC" | "ASC"} 排序字段
|
||||
*/
|
||||
"list": {
|
||||
"path": "mark",
|
||||
"key": "list",
|
||||
"description": "获取mark列表",
|
||||
"metadata": {
|
||||
"args": {
|
||||
"page": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "页码",
|
||||
"type": "number",
|
||||
"optional": true
|
||||
},
|
||||
"pageSize": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "每页数量",
|
||||
"type": "number",
|
||||
"optional": true
|
||||
},
|
||||
"search": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "搜索关键词",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"markType": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "mark类型,simple,wallnote,md,draw等",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"sort": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"default": "DESC",
|
||||
"description": "排序字段",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"DESC",
|
||||
"ASC"
|
||||
],
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"url": "/api/router",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 获取mark版本信息
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.id - {string} mark id
|
||||
*/
|
||||
"getVersion": {
|
||||
"path": "mark",
|
||||
"key": "getVersion",
|
||||
"description": "获取mark版本信息",
|
||||
"metadata": {
|
||||
"args": {
|
||||
"id": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "mark id"
|
||||
}
|
||||
},
|
||||
"url": "/api/router",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 获取mark详情
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.id - {string} mark id
|
||||
*/
|
||||
"get": {
|
||||
"path": "mark",
|
||||
"key": "get",
|
||||
"description": "获取mark详情",
|
||||
"metadata": {
|
||||
"args": {
|
||||
"id": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "mark id"
|
||||
}
|
||||
},
|
||||
"url": "/api/router",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 更新mark内容
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.id - {string} mark id
|
||||
* @param data.data - {object}
|
||||
*/
|
||||
"update": {
|
||||
"path": "mark",
|
||||
"key": "update",
|
||||
"description": "更新mark内容",
|
||||
"metadata": {
|
||||
"args": {
|
||||
"id": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "mark id"
|
||||
},
|
||||
"data": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"default": "",
|
||||
"type": "string"
|
||||
},
|
||||
"tags": {
|
||||
"default": []
|
||||
},
|
||||
"link": {
|
||||
"default": "",
|
||||
"type": "string"
|
||||
},
|
||||
"summary": {
|
||||
"default": "",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"default": "",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title",
|
||||
"tags",
|
||||
"link",
|
||||
"summary",
|
||||
"description"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"url": "/api/router",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 更新mark节点,支持更新和删除操作
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.id - {string} mark id
|
||||
* @param data.operate - {"update" | "delete"} 节点操作类型,update或delete
|
||||
* @param data.data - {object} 要更新的节点数据
|
||||
*/
|
||||
"updateNode": {
|
||||
"path": "mark",
|
||||
"key": "updateNode",
|
||||
"description": "更新mark节点,支持更新和删除操作",
|
||||
"metadata": {
|
||||
"args": {
|
||||
"id": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "mark id"
|
||||
},
|
||||
"operate": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"default": "update",
|
||||
"description": "节点操作类型,update或delete",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"update",
|
||||
"delete"
|
||||
],
|
||||
"optional": true
|
||||
},
|
||||
"data": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "节点id"
|
||||
},
|
||||
"node": {
|
||||
"description": "要更新的节点数据"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"node"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"description": "要更新的节点数据"
|
||||
}
|
||||
},
|
||||
"url": "/api/router",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 批量更新mark节点,支持更新和删除操作
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.id - {string} mark id
|
||||
* @param data.nodeOperateList - {array} 要更新的节点列表
|
||||
*/
|
||||
"updateNodes": {
|
||||
"path": "mark",
|
||||
"key": "updateNodes",
|
||||
"description": "批量更新mark节点,支持更新和删除操作",
|
||||
"metadata": {
|
||||
"args": {
|
||||
"id": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "mark id"
|
||||
},
|
||||
"nodeOperateList": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"operate": {
|
||||
"default": "update",
|
||||
"description": "节点操作类型,update或delete",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"update",
|
||||
"delete"
|
||||
]
|
||||
},
|
||||
"node": {
|
||||
"description": "要更新的节点数据"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"operate",
|
||||
"node"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"description": "要更新的节点列表"
|
||||
}
|
||||
},
|
||||
"url": "/api/router",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param data - Request parameters
|
||||
* @param data.id - {string} mark id
|
||||
*/
|
||||
"delete": {
|
||||
"path": "mark",
|
||||
"key": "delete",
|
||||
"metadata": {
|
||||
"args": {
|
||||
"id": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "mark id"
|
||||
}
|
||||
},
|
||||
"url": "/api/router",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 创建一个新的mark.
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.title - {string} 标题
|
||||
* @param data.tags - {unknown} 标签
|
||||
* @param data.link - {string} 链接
|
||||
* @param data.summary - {string} 摘要
|
||||
* @param data.description - {string} 描述
|
||||
* @param data.markType - {string} mark类型
|
||||
* @param data.config - {unknown} 配置
|
||||
* @param data.data - {unknown} 数据
|
||||
*/
|
||||
"create": {
|
||||
"path": "mark",
|
||||
"key": "create",
|
||||
"description": "创建一个新的mark.",
|
||||
"metadata": {
|
||||
"args": {
|
||||
"title": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"default": "",
|
||||
"description": "标题",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"tags": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"default": [],
|
||||
"description": "标签",
|
||||
"optional": true
|
||||
},
|
||||
"link": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"default": "",
|
||||
"description": "链接",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"summary": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"default": "",
|
||||
"description": "摘要",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"description": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"default": "",
|
||||
"description": "描述",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"markType": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"default": "md",
|
||||
"description": "mark类型",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"config": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"default": {},
|
||||
"description": "配置",
|
||||
"optional": true
|
||||
},
|
||||
"data": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"default": {},
|
||||
"description": "数据",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"url": "/api/router",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 获取mark菜单
|
||||
*/
|
||||
"getMenu": {
|
||||
"path": "mark",
|
||||
"key": "getMenu",
|
||||
"description": "获取mark菜单",
|
||||
"metadata": {
|
||||
"url": "/api/router",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
}
|
||||
}
|
||||
} as const;
|
||||
const queryApi = createQueryApi({ api });
|
||||
|
||||
export { queryApi };
|
||||
@@ -1,3 +1,18 @@
|
||||
import { QueryClient } from '@kevisual/query';
|
||||
import { Query, DataOpts } from '@kevisual/query';
|
||||
import { QueryLoginBrowser } from '@kevisual/api/query-login'
|
||||
import { useContextKey } from '@kevisual/context';
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
|
||||
export const query = new QueryClient();
|
||||
export const query = useContextKey('query', new Query({
|
||||
url: '/api/router',
|
||||
}));
|
||||
|
||||
export const queryClient = useContextKey('queryClient', new Query({
|
||||
url: '/client/router',
|
||||
}));
|
||||
|
||||
export const queryLogin = useContextKey('queryLogin', new QueryLoginBrowser({
|
||||
query: query
|
||||
}));
|
||||
|
||||
export const stackQueryClient = useContextKey('stackQueryClient', new QueryClient());
|
||||
@@ -1,3 +0,0 @@
|
||||
export const Home = () => {
|
||||
return <div>Home Page</div>
|
||||
}
|
||||
43
src/pages/ai/page.tsx
Normal file
43
src/pages/ai/page.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { CNB, Issue } from '@kevisual/cnb'
|
||||
import { useLayoutEffect } from 'react'
|
||||
import { useConfigStore } from '../config/store'
|
||||
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
||||
import { generateText } from 'ai';
|
||||
const init2 = async () => {
|
||||
const cnb = new CNB({
|
||||
token: 'cIDfLOOIr1Trt15cdnwfndupEZG',
|
||||
cookie: 'CNBSESSION=1770014410.1935321989751226368.7f386c282d80efb5256180ef94c2865e20a8be72e2927a5f8eb1eb72142de39f;csrfkey=2028873452',
|
||||
cors: {
|
||||
baseUrl: 'https://cors.kevisual.cn'
|
||||
}
|
||||
})
|
||||
// const res = await cnb.issue.getList('kevisual/kevisual')
|
||||
// console.log('res', res)
|
||||
const token = await cnb.user.getCurrentUser()
|
||||
console.log('token', token)
|
||||
}
|
||||
|
||||
const initAi = async () => {
|
||||
const state = useConfigStore.getState()
|
||||
const config = state.config
|
||||
const cors = state.config.CNB_CORS_URL
|
||||
const base = cors + '/' + config.AI_BASE_URL.replace('https://', '')
|
||||
const cnb = createOpenAICompatible({
|
||||
baseURL: base,
|
||||
name: 'custom-cnb',
|
||||
apiKey: config.AI_API_KEY,
|
||||
});
|
||||
const model = config.AI_MODEL;
|
||||
// const model = 'hunyuan';
|
||||
const { text } = await generateText({
|
||||
model: cnb(model),
|
||||
prompt: '你好',
|
||||
});
|
||||
console.log('text', text)
|
||||
}
|
||||
export const Home = () => {
|
||||
useLayoutEffect(() => { initAi() }, [])
|
||||
return <div>Home Page</div>
|
||||
}
|
||||
|
||||
export default Home;
|
||||
1
src/pages/auth/hooks/index.ts
Normal file
1
src/pages/auth/hooks/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './use-api-query';
|
||||
55
src/pages/auth/hooks/use-api-query.ts
Normal file
55
src/pages/auth/hooks/use-api-query.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { queryLogin } from '@/modules/query';
|
||||
import { toast } from 'sonner';
|
||||
import type { UserInfo } from '../store';
|
||||
|
||||
export const authQueryKeys = {
|
||||
me: ['auth', 'me'] as const,
|
||||
token: ['auth', 'token'] as const,
|
||||
} as const;
|
||||
|
||||
export const useMe = () => {
|
||||
return useQuery({
|
||||
queryKey: authQueryKeys.me,
|
||||
queryFn: async () => {
|
||||
const res = await queryLogin.getMe();
|
||||
if (res.code === 200) {
|
||||
return res.data;
|
||||
}
|
||||
throw new Error(res.message || 'Failed to fetch user info');
|
||||
},
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
});
|
||||
};
|
||||
|
||||
export const useSwitchOrg = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (username?: string) => {
|
||||
const res = await queryLogin.switchUser(username || '');
|
||||
if (res.code === 200) {
|
||||
return res.data;
|
||||
}
|
||||
throw new Error(res.message || 'Switch failed');
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('切换成功');
|
||||
queryClient.invalidateQueries({ queryKey: authQueryKeys.me });
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message || '请求失败');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetToken = () => {
|
||||
return useQuery({
|
||||
queryKey: authQueryKeys.token,
|
||||
queryFn: () => queryLogin.getToken(),
|
||||
staleTime: Infinity,
|
||||
});
|
||||
};
|
||||
57
src/pages/auth/index.tsx
Normal file
57
src/pages/auth/index.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { useEffect } from "react"
|
||||
import { useLayoutStore } from "./store"
|
||||
import { useShallow } from "zustand/shallow"
|
||||
import { LogIn, LockKeyhole } from "lucide-react"
|
||||
export { BaseHeader } from './modules/BaseHeader'
|
||||
import { useMemo } from 'react';
|
||||
import { useLocation, useNavigate } from '@tanstack/react-router';
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode,
|
||||
mustLogin?: boolean,
|
||||
}
|
||||
export const AuthProvider = ({ children, mustLogin }: Props) => {
|
||||
const store = useLayoutStore(useShallow(state => ({
|
||||
init: state.init,
|
||||
me: state.me,
|
||||
openLinkList: state.openLinkList,
|
||||
})));
|
||||
useEffect(() => {
|
||||
store.init()
|
||||
}, [])
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate();
|
||||
const isOpen = useMemo(() => {
|
||||
return store.openLinkList.some(item => location.pathname.startsWith(item))
|
||||
}, [location.pathname])
|
||||
const loginUrl = '/root/login/?redirect=' + encodeURIComponent(window.location.href);
|
||||
if (mustLogin && !store.me && !isOpen) {
|
||||
return (
|
||||
<div className="w-full h-full min-h-screen flex items-center justify-center bg-background">
|
||||
<div className="flex flex-col items-center gap-6 p-10 rounded-2xl border border-border bg-card shadow-lg max-w-sm w-full mx-4">
|
||||
<div className="flex items-center justify-center w-16 h-16 rounded-full bg-muted">
|
||||
<LockKeyhole className="w-8 h-8 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<h2 className="text-xl font-semibold text-foreground">需要登录</h2>
|
||||
<p className="text-sm text-muted-foreground">请先登录以继续访问此页面</p>
|
||||
</div>
|
||||
<div
|
||||
className="inline-flex items-center justify-center gap-2 w-full px-6 py-2.5 rounded-lg bg-foreground text-background text-sm font-medium transition-opacity hover:opacity-80 active:opacity-70"
|
||||
onClick={() => {
|
||||
// window.open(loginUrl, '_blank');
|
||||
navigate({ to: '/login' });
|
||||
}}
|
||||
>
|
||||
<LogIn className="w-4 h-4" />
|
||||
立即登录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return <>
|
||||
{children}
|
||||
</>
|
||||
}
|
||||
92
src/pages/auth/modules/BaseHeader.tsx
Normal file
92
src/pages/auth/modules/BaseHeader.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Home, User, LogIn, LogOut } from 'lucide-react';
|
||||
import { Link, useNavigate } from '@tanstack/react-router'
|
||||
import { useLayoutStore } from '../store';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const BaseHeader = (props: { main?: React.ComponentType | null }) => {
|
||||
const store = useLayoutStore(useShallow(state => ({
|
||||
me: state.me,
|
||||
clearMe: state.clearMe,
|
||||
links: state.links,
|
||||
showBaseHeader: state.showBaseHeader,
|
||||
})));
|
||||
const navigate = useNavigate();
|
||||
const meInfo = useMemo(() => {
|
||||
if (!store.me) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => navigate({ to: '/login' })}
|
||||
className="flex items-center gap-2 px-3 py-1.5 text-sm text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
|
||||
>
|
||||
<LogIn className="w-4 h-4" />
|
||||
<span>去登录</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
{store.me.avatar && (
|
||||
<img
|
||||
src={store.me.avatar}
|
||||
alt="Avatar"
|
||||
className="w-8 h-8 rounded-full object-cover"
|
||||
/>
|
||||
)}
|
||||
{!store.me.avatar && (
|
||||
<div className="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center">
|
||||
<User className="w-4 h-4 text-gray-500" />
|
||||
</div>
|
||||
)}
|
||||
<span className="font-medium text-gray-700">{store.me?.username}</span>
|
||||
<button
|
||||
onClick={() => store.clearMe?.()}
|
||||
className="flex items-center gap-1 px-2 py-1 text-sm text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors cursor-pointer"
|
||||
title="退出登录"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}, [store.me, store.clearMe])
|
||||
if (!store.showBaseHeader) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="flex gap-2 text-lg w-full h-12 items-center justify-between bg-gray-200">
|
||||
<div className='px-2 flex items-center gap-1'>
|
||||
{
|
||||
store.links.map(link => (
|
||||
<div key={link.key || link.title}
|
||||
className="cursor-pointer flex items-center justify-center gap-1 p-2 text-sm text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
onClick={() => {
|
||||
if (!link.href) return;
|
||||
if (link.href.startsWith('http') || link.isRoot) {
|
||||
window.open(link.href, '_blank');
|
||||
return;
|
||||
}
|
||||
navigate({
|
||||
to: link.href
|
||||
})
|
||||
}}
|
||||
>
|
||||
{link.key === 'home' && <Home className="w-4 h-4" />}
|
||||
{link.icon && <>{link.icon}</>}
|
||||
{!link.icon && link.title}
|
||||
</div>
|
||||
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div className='mr-4'>
|
||||
{meInfo}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const LayoutMain = () => {
|
||||
return <BaseHeader />
|
||||
}
|
||||
81
src/pages/auth/page.tsx
Normal file
81
src/pages/auth/page.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { useContextKey } from '@kevisual/context';
|
||||
import '@kevisual/kv-login';
|
||||
import { checkPluginLogin } from '@kevisual/kv-login'
|
||||
import { useEffect } from 'react';
|
||||
import { useLayoutStore } from './store';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
|
||||
export const LoginComponent = ({ onLoginSuccess }: { onLoginSuccess: () => void }) => {
|
||||
useEffect(() => {
|
||||
// 监听登录成功事件
|
||||
const handleLoginSuccess = () => {
|
||||
console.log('监听到登录成功事件,关闭弹窗');
|
||||
onLoginSuccess();
|
||||
};
|
||||
const loginEmitter = useContextKey('login-emitter')
|
||||
console.log('KvLogin Types:', loginEmitter);
|
||||
|
||||
loginEmitter.on('login-success', handleLoginSuccess);
|
||||
|
||||
// 清理监听器
|
||||
return () => {
|
||||
loginEmitter.off('login-success', handleLoginSuccess);
|
||||
};
|
||||
}, [onLoginSuccess]);
|
||||
|
||||
// @ts-ignore
|
||||
return (<kv-login></kv-login>)
|
||||
}
|
||||
export const App = () => {
|
||||
const store = useLayoutStore(useShallow((state) => ({
|
||||
init: state.init,
|
||||
loginPageConfig: state.loginPageConfig,
|
||||
})));
|
||||
useEffect(() => {
|
||||
checkPluginLogin();
|
||||
}, []);
|
||||
const navigate = useNavigate();
|
||||
const handleLoginSuccess = async () => {
|
||||
await store.init()
|
||||
navigate({ to: '/' })
|
||||
};
|
||||
const { title, subtitle, footer } = store.loginPageConfig;
|
||||
return (
|
||||
<div className='w-full h-full relative overflow-hidden bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900'>
|
||||
{/* 背景装饰 - 圆形光晕 */}
|
||||
<div className='absolute top-1/4 -left-32 w-96 h-96 bg-purple-500/30 rounded-full blur-3xl'></div>
|
||||
<div className='absolute bottom-1/4 -right-32 w-96 h-96 bg-blue-500/30 rounded-full blur-3xl'></div>
|
||||
<div className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-indigo-500/20 rounded-full blur-3xl'></div>
|
||||
|
||||
{/* 背景装饰 - 网格图案 */}
|
||||
<div className='absolute inset-0 opacity-[0.03] bg-[linear-gradient(rgba(255,255,255,0.1)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.1)_1px,transparent_1px)] bg-[size:50px_50px]'></div>
|
||||
|
||||
{/* 顶部装饰文字 */}
|
||||
<div className='absolute top-10 left-0 right-0 text-center'>
|
||||
<h1 className='text-4xl font-bold text-white/90 tracking-wider'>{title}</h1>
|
||||
<p className='mt-2 text-white/50 text-sm tracking-widest'>{subtitle}</p>
|
||||
</div>
|
||||
|
||||
{/* 登录卡片容器 */}
|
||||
<div className='w-full h-full flex items-center justify-center p-8'>
|
||||
<div className='relative'>
|
||||
{/* 卡片外圈光效 */}
|
||||
<div className='absolute -inset-1 bg-gradient-to-r from-purple-500 via-blue-500 to-indigo-500 rounded-2xl blur opacity-30'></div>
|
||||
|
||||
{/* 登录组件容器 */}
|
||||
<div className='relative bg-slate-900/80 backdrop-blur-xl rounded-2xl border border-white/10 shadow-2xl overflow-hidden'>
|
||||
<LoginComponent onLoginSuccess={handleLoginSuccess} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 底部装饰 */}
|
||||
<div className='absolute bottom-6 left-0 right-0 text-center'>
|
||||
<p className='text-white/30 text-xs'>{footer}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App;
|
||||
134
src/pages/auth/store.ts
Normal file
134
src/pages/auth/store.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
|
||||
import { queryLogin, stackQueryClient } from '@/modules/query';
|
||||
import { create } from 'zustand';
|
||||
import { toast } from 'sonner';
|
||||
import { authQueryKeys } from './hooks';
|
||||
export type UserInfo = {
|
||||
id?: string;
|
||||
username?: string;
|
||||
nickname?: string | null;
|
||||
needChangePassword?: boolean;
|
||||
description?: string | null;
|
||||
type?: 'user' | 'org';
|
||||
orgs?: string[];
|
||||
avatar?: string;
|
||||
};
|
||||
export type LayoutStore = {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
openUser: boolean;
|
||||
setOpenUser: (openUser: boolean) => void;
|
||||
me?: UserInfo;
|
||||
setMe: (me: UserInfo) => void;
|
||||
clearMe: () => void;
|
||||
getMe: () => Promise<void>;
|
||||
switchOrg: (username?: string) => Promise<void>;
|
||||
isAdmin: boolean;
|
||||
setIsAdmin: (isAdmin: boolean) => void
|
||||
init: () => Promise<void>;
|
||||
openLinkList: string[];
|
||||
setOpenLinkList: (openLinkList: string[]) => void;
|
||||
loginPageConfig: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
footer: string;
|
||||
};
|
||||
setLoginPageConfig: (config: Partial<LayoutStore['loginPageConfig']>) => void;
|
||||
links: HeaderLink[];
|
||||
setLinks: (links: HeaderLink[]) => void;
|
||||
showBaseHeader: boolean;
|
||||
setShowBaseHeader: (showBaseHeader: boolean) => void;
|
||||
serverData: Record<string, any> | null;
|
||||
setServerData: (data: Record<string, any>) => void;
|
||||
initConvex: () => Promise<void>;
|
||||
};
|
||||
type HeaderLink = {
|
||||
title?: string;
|
||||
href: string;
|
||||
description?: string;
|
||||
icon?: React.ReactNode;
|
||||
key?: string;
|
||||
isRoot?: boolean;
|
||||
};
|
||||
|
||||
export const useLayoutStore = create<LayoutStore>((set, get) => ({
|
||||
open: false,
|
||||
setOpen: (open) => set({ open }),
|
||||
openUser: false,
|
||||
setOpenUser: (openUser) => set({ openUser }),
|
||||
me: undefined,
|
||||
setMe: (me) => set({ me }),
|
||||
clearMe: () => {
|
||||
set({ me: undefined, isAdmin: false });
|
||||
},
|
||||
getMe: async () => {
|
||||
const data = await stackQueryClient.fetchQuery({
|
||||
queryKey: authQueryKeys.me,
|
||||
queryFn: async () => {
|
||||
const res = await queryLogin.getMe();
|
||||
if (res.code === 200) {
|
||||
return res.data;
|
||||
}
|
||||
throw new Error(res.message || 'Failed to fetch user info');
|
||||
},
|
||||
});
|
||||
set({ me: data, isAdmin: data?.orgs?.includes?.('admin') || false });
|
||||
},
|
||||
switchOrg: async (username?: string) => {
|
||||
const res = await queryLogin.switchUser(username || '');
|
||||
if (res.code === 200) {
|
||||
toast.success('切换成功');
|
||||
stackQueryClient.invalidateQueries({ queryKey: authQueryKeys.me });
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
toast.error(res.message || '请求失败');
|
||||
}
|
||||
},
|
||||
isAdmin: false,
|
||||
setIsAdmin: (isAdmin) => set({ isAdmin }),
|
||||
init: async () => {
|
||||
await queryLogin.init();
|
||||
const token = await queryLogin.checkLocalToken();
|
||||
if (token) {
|
||||
set({ me: {} });
|
||||
try {
|
||||
// const data = await stackQueryClient.fetchQuery({
|
||||
// queryKey: authQueryKeys.me,
|
||||
// }) as UserInfo;
|
||||
const userInfo = await queryLogin.checkLocalUser();
|
||||
if (userInfo) {
|
||||
set({ me: userInfo as UserInfo, isAdmin: userInfo.orgs?.includes?.('admin') || false });
|
||||
} else {
|
||||
set({ me: undefined, isAdmin: false });
|
||||
}
|
||||
} catch {
|
||||
set({ me: undefined, isAdmin: false });
|
||||
}
|
||||
}
|
||||
// 获取服务端数据
|
||||
// @ts-ignore
|
||||
const sererData = window.__SERVER_DATA__;
|
||||
if (sererData) {
|
||||
set({ serverData: sererData });
|
||||
}
|
||||
},
|
||||
initConvex: async () => { },
|
||||
openLinkList: ['/login'],
|
||||
setOpenLinkList: (openLinkList) => set({ openLinkList }),
|
||||
loginPageConfig: {
|
||||
title: '可视化管理平台',
|
||||
subtitle: '让工具和智能化触手可及',
|
||||
footer: '欢迎使用可视化管理平台 · 连接您的工具',
|
||||
},
|
||||
setLoginPageConfig: (config) => set((state) => ({
|
||||
loginPageConfig: { ...state.loginPageConfig, ...config },
|
||||
})),
|
||||
links: [{ title: '', href: '/', key: 'home' }],
|
||||
setLinks: (links) => set({ links }),
|
||||
showBaseHeader: true,
|
||||
setShowBaseHeader: (showBaseHeader) => set({ showBaseHeader }),
|
||||
serverData: null,
|
||||
setServerData: (data) => set({ serverData: data }),
|
||||
}));
|
||||
239
src/pages/cloud-env/page.tsx
Normal file
239
src/pages/cloud-env/page.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useCloudEnvStore } from './store'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { SidebarLayout } from '../sidebar/components'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip'
|
||||
import {
|
||||
Code2,
|
||||
Terminal,
|
||||
MousePointer2,
|
||||
Lock,
|
||||
Radio,
|
||||
Zap,
|
||||
Square,
|
||||
RefreshCw,
|
||||
Copy,
|
||||
Check,
|
||||
Wind,
|
||||
Plane,
|
||||
Rocket,
|
||||
ExternalLink
|
||||
} from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { WorkspaceInfo } from '@kevisual/cnb'
|
||||
import clsx from 'clsx'
|
||||
|
||||
type WorkspaceOpen = {
|
||||
url?: string
|
||||
webide?: string
|
||||
jumpUrl?: string
|
||||
remoteSsh?: string
|
||||
jetbrains?: Record<string, string>
|
||||
codebuddy?: string
|
||||
codebuddycn?: string
|
||||
vscode?: string
|
||||
cursor?: string
|
||||
'vscode-insiders'?: string
|
||||
trae?: string
|
||||
'trae-cn'?: string
|
||||
windsurf?: string
|
||||
'windsurf-next'?: string
|
||||
antigravity?: string
|
||||
ssh?: string
|
||||
}
|
||||
|
||||
interface LinkItem {
|
||||
key: string
|
||||
label: string
|
||||
icon: React.ReactNode
|
||||
getUrl: (data: WorkspaceOpen) => string | undefined
|
||||
}
|
||||
|
||||
const linkItems: LinkItem[] = [
|
||||
{ key: 'jumpUrl', label: 'Jump', icon: <ExternalLink className="w-5 h-5" />, getUrl: (d) => d.jumpUrl },
|
||||
{ key: 'webide', label: 'Web IDE', icon: <Code2 className="w-5 h-5" />, getUrl: (d) => d.webide },
|
||||
{ key: 'vscode', label: 'VS Code', icon: <Code2 className="w-5 h-5" />, getUrl: (d) => d.vscode },
|
||||
{ key: 'cursor', label: 'Cursor', icon: <MousePointer2 className="w-5 h-5" />, getUrl: (d) => d.cursor },
|
||||
{ key: 'trae-cn', label: 'Trae', icon: <Rocket className="w-5 h-5" />, getUrl: (d) => d['trae-cn'] },
|
||||
{ key: 'windsurf', label: 'Windsurf', icon: <Wind className="w-5 h-5" />, getUrl: (d) => d.windsurf },
|
||||
{ key: 'antigravity', label: 'Antigravity', icon: <Plane className="w-5 h-5" />, getUrl: (d) => d.antigravity },
|
||||
{ key: 'ssh', label: 'SSH', icon: <Lock className="w-5 h-5" />, getUrl: (d) => d.ssh },
|
||||
{ key: 'remoteSsh', label: 'Remote SSH', icon: <Radio className="w-5 h-5" />, getUrl: (d) => d.remoteSsh },
|
||||
{ key: 'codebuddycn', label: 'CodeBuddy', icon: <Zap className="w-5 h-5" />, getUrl: (d) => d.codebuddycn },
|
||||
]
|
||||
|
||||
function LinkCard({ item, workspaceData }: { item: LinkItem; workspaceData: WorkspaceOpen }) {
|
||||
const url = item.getUrl(workspaceData)
|
||||
const [copied, setCopied] = useState(false)
|
||||
if (!url) return null
|
||||
|
||||
const handleClick = () => {
|
||||
if (url.startsWith('ssh') || url.startsWith('cnb')) {
|
||||
return
|
||||
}
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
const handleCopy = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
try {
|
||||
await navigator.clipboard.writeText(url)
|
||||
setCopied(true)
|
||||
toast.success('已复制')
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch {
|
||||
toast.error('复制失败')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div
|
||||
onClick={handleClick}
|
||||
className={clsx(
|
||||
'flex items-center gap-2 p-2.5 rounded-lg border border-neutral-200 transition-all cursor-pointer',
|
||||
'hover:border-neutral-900 hover:bg-neutral-50'
|
||||
)}
|
||||
>
|
||||
<div className="text-neutral-700 shrink-0">{item.icon}</div>
|
||||
<span className="text-sm font-medium text-neutral-900 truncate flex-1">{item.label}</span>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="p-1 rounded hover:bg-neutral-100 shrink-0"
|
||||
>
|
||||
{copied ? <Check className="w-4 h-4 text-green-600" /> : <Copy className="w-4 h-4 text-neutral-400" />}
|
||||
</button>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="max-w-xs break-all">{url}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function WorkspaceCard({ workspace, onStop }: { workspace: WorkspaceInfo; onStop: (ws: WorkspaceInfo) => void }) {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [workspaceData, setWorkspaceData] = useState<WorkspaceOpen | null>(null)
|
||||
const getWorkspaceDetail = useCloudEnvStore((state) => state.getWorkspaceDetail)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDetail = async () => {
|
||||
setLoading(true)
|
||||
const data = await getWorkspaceDetail(workspace)
|
||||
setWorkspaceData(data)
|
||||
setLoading(false)
|
||||
}
|
||||
fetchDetail()
|
||||
}, [workspace, getWorkspaceDetail])
|
||||
|
||||
return (
|
||||
<Card className="p-4 space-y-4 border border-neutral-200 bg-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="relative flex h-2.5 w-2.5">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-green-500"></span>
|
||||
</span>
|
||||
<span className="font-medium text-neutral-900">{workspace.slug}</span>
|
||||
</div>
|
||||
{workspace.branch && (
|
||||
<Badge variant="outline" className="text-xs">{workspace.branch}</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => onStop(workspace)}
|
||||
className="text-red-600 border-red-200 hover:bg-red-600 hover:text-white"
|
||||
>
|
||||
<Square className="w-4 h-4 mr-1" />
|
||||
停止
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<div key={i} className="h-12 bg-neutral-100 rounded-lg animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
) : workspaceData ? (
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{linkItems.map((item) => (
|
||||
<LinkCard key={item.key} item={item} workspaceData={workspaceData} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-neutral-400 py-4">暂无链接信息</div>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default function CloudEnvPage() {
|
||||
const { workspaceList, loading, getWorkspaceList, stopWorkspace } = useCloudEnvStore()
|
||||
|
||||
useEffect(() => {
|
||||
getWorkspaceList()
|
||||
}, [getWorkspaceList])
|
||||
|
||||
return (
|
||||
<SidebarLayout>
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-neutral-900">云端开发环境</h1>
|
||||
<p className="text-neutral-500 mt-1">当前运行中的云端开发环境</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => getWorkspaceList()}
|
||||
disabled={loading}
|
||||
>
|
||||
<RefreshCw className={clsx('w-4 h-4 mr-2', loading && 'animate-spin')} />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{loading && workspaceList.length === 0 ? (
|
||||
<div className="space-y-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<Skeleton key={i} className="h-40" />
|
||||
))}
|
||||
</div>
|
||||
) : workspaceList.length === 0 ? (
|
||||
<Card className="p-12 text-center border border-neutral-200">
|
||||
<div className="text-neutral-400 mb-4">
|
||||
<Terminal className="w-12 h-12 mx-auto mb-4" />
|
||||
<p className="text-lg font-medium">暂无运行中的工作区</p>
|
||||
<p className="text-sm mt-1">在仓库管理页面启动工作区即可在此查看</p>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid gap-4">
|
||||
{workspaceList.map((workspace) => (
|
||||
<WorkspaceCard
|
||||
key={workspace.sn}
|
||||
workspace={workspace}
|
||||
onStop={stopWorkspace}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SidebarLayout>
|
||||
)
|
||||
}
|
||||
91
src/pages/cloud-env/store/index.ts
Normal file
91
src/pages/cloud-env/store/index.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { create } from 'zustand'
|
||||
import { toast } from 'sonner'
|
||||
import { queryApi as cnbApi } from '@/modules/cnb-api'
|
||||
import { WorkspaceInfo } from '@kevisual/cnb'
|
||||
|
||||
type WorkspaceOpen = {
|
||||
url?: string
|
||||
webide?: string
|
||||
jumpUrl?: string
|
||||
remoteSsh?: string
|
||||
jetbrains?: Record<string, string>
|
||||
codebuddy?: string
|
||||
codebuddycn?: string
|
||||
vscode?: string
|
||||
cursor?: string
|
||||
'vscode-insiders'?: string
|
||||
trae?: string
|
||||
'trae-cn'?: string
|
||||
windsurf?: string
|
||||
'windsurf-next'?: string
|
||||
antigravity?: string
|
||||
ssh?: string
|
||||
}
|
||||
|
||||
type State = {
|
||||
workspaceList: WorkspaceInfo[]
|
||||
loading: boolean
|
||||
getWorkspaceList: () => Promise<void>
|
||||
getWorkspaceDetail: (data: WorkspaceInfo) => Promise<WorkspaceOpen | null>
|
||||
stopWorkspace: (workspace: WorkspaceInfo) => Promise<void>
|
||||
}
|
||||
|
||||
export const useCloudEnvStore = create<State>((set, get) => ({
|
||||
workspaceList: [],
|
||||
loading: false,
|
||||
getWorkspaceList: async () => {
|
||||
set({ loading: true })
|
||||
try {
|
||||
const res = await cnbApi.cnb['list-workspace']({
|
||||
status: 'running',
|
||||
pageSize: 100
|
||||
})
|
||||
if (res.code === 200) {
|
||||
const list: WorkspaceInfo[] = res.data?.list || []
|
||||
set({ workspaceList: list })
|
||||
} else {
|
||||
toast.error(res.message || '请求失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取工作区列表失败:', error)
|
||||
toast.error('获取工作区列表失败')
|
||||
} finally {
|
||||
set({ loading: false })
|
||||
}
|
||||
},
|
||||
getWorkspaceDetail: async (workspaceInfo: WorkspaceInfo): Promise<WorkspaceOpen | null> => {
|
||||
try {
|
||||
const res = await cnbApi.cnb['get-workspace']({
|
||||
repo: workspaceInfo.slug,
|
||||
sn: workspaceInfo.sn
|
||||
}) as any
|
||||
if (res.code === 200) {
|
||||
return res.data
|
||||
}
|
||||
return null
|
||||
} catch (error) {
|
||||
console.error('获取工作区详情失败:', error)
|
||||
return null
|
||||
}
|
||||
},
|
||||
stopWorkspace: async (workspace: WorkspaceInfo) => {
|
||||
const sn = workspace.sn
|
||||
if (!sn) {
|
||||
toast.error('工作区 SN 不存在')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await cnbApi.cnb['stop-workspace']({ sn })
|
||||
if (res?.code === 200) {
|
||||
toast.success('工作区已停止')
|
||||
// 刷新列表
|
||||
await get().getWorkspaceList()
|
||||
} else {
|
||||
toast.error(res.message || '停止失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('停止工作区失败:', error)
|
||||
toast.error('停止失败')
|
||||
}
|
||||
}
|
||||
}))
|
||||
906
src/pages/cnb/api.ts
Normal file
906
src/pages/cnb/api.ts
Normal file
@@ -0,0 +1,906 @@
|
||||
import { createQueryApi } from '@kevisual/query/api';
|
||||
import { queryClient as query } from '@/modules/query.ts';
|
||||
const api = {
|
||||
"cnb": {
|
||||
/**
|
||||
* 验证 CNB 登录信息是否有效
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.checkToken - {boolean} 是否检查 Token 的有效性
|
||||
* @param data.checkCookie - {boolean} 是否检查 Cookie 的有效性
|
||||
*/
|
||||
"user-check": {
|
||||
"path": "cnb",
|
||||
"key": "user-check",
|
||||
"description": "检查用户登录状态,参数checkToken,default true; checkCookie, default false",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"opencode"
|
||||
],
|
||||
"args": {
|
||||
"checkToken": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"default": true,
|
||||
"type": "boolean",
|
||||
"description": "是否检查 Token 的有效性",
|
||||
"optional": true
|
||||
},
|
||||
"checkCookie": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"default": false,
|
||||
"type": "boolean",
|
||||
"description": "是否检查 Cookie 的有效性",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"skill": "cnb-login-verify",
|
||||
"title": "CNB 登录验证信息",
|
||||
"summary": "验证 CNB 登录信息是否有效",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 列出cnb代码仓库, 可选flags参数,如 KnowledgeBase
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.search - {string} 搜索关键词
|
||||
* @param data.pageSize - {number} 每页数量,默认999
|
||||
* @param data.flags - {string} 仓库标记,如果是知识库则填写 KnowledgeBase
|
||||
*/
|
||||
"list-repos": {
|
||||
"path": "cnb",
|
||||
"key": "list-repos",
|
||||
"description": "列出我的代码仓库",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"opencode"
|
||||
],
|
||||
"args": {
|
||||
"search": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "搜索关键词",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"pageSize": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "每页数量,默认999",
|
||||
"type": "number",
|
||||
"optional": true
|
||||
},
|
||||
"flags": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "仓库标记,如果是知识库则填写 KnowledgeBase",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"skill": "list-repos",
|
||||
"title": "列出cnb代码仓库",
|
||||
"summary": "列出cnb代码仓库, 可选flags参数,如 KnowledgeBase",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 创建一个新的代码仓库
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.name - {string} 代码仓库名称, 如 my-user/my-repo
|
||||
* @param data.visibility - {string} 代码仓库可见性, public 或 private
|
||||
* @param data.description - {string} 代码仓库描述
|
||||
*/
|
||||
"create-repo": {
|
||||
"path": "cnb",
|
||||
"key": "create-repo",
|
||||
"description": "创建代码仓库, 参数name, visibility, description",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"opencode"
|
||||
],
|
||||
"args": {
|
||||
"name": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "代码仓库名称, 如 my-user/my-repo"
|
||||
},
|
||||
"visibility": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"default": "public",
|
||||
"type": "string",
|
||||
"description": "代码仓库可见性, public 或 private",
|
||||
"optional": true
|
||||
},
|
||||
"description": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "代码仓库描述"
|
||||
}
|
||||
},
|
||||
"skill": "create-repo",
|
||||
"title": "创建代码仓库",
|
||||
"summary": "创建一个新的代码仓库",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 在代码仓库中创建文件, encoding 可选,默认 raw
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.repoName - {string} 代码仓库名称, 如 my-user/my-repo
|
||||
* @param data.filePath - {string} 文件路径, 如 src/index.ts
|
||||
* @param data.content - {string} 文本的字符串的内容
|
||||
* @param data.encoding - {string} 编码方式,如 raw
|
||||
*/
|
||||
"create-repo-file": {
|
||||
"path": "cnb",
|
||||
"key": "create-repo-file",
|
||||
"description": "在代码仓库中创建文件, repoName, filePath, content, encoding。使用CNB_COOKIE进行鉴权",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"opencode"
|
||||
],
|
||||
"args": {
|
||||
"repoName": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "代码仓库名称, 如 my-user/my-repo"
|
||||
},
|
||||
"filePath": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "文件路径, 如 src/index.ts"
|
||||
},
|
||||
"content": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "文本的字符串的内容"
|
||||
},
|
||||
"encoding": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "编码方式,如 raw",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"skill": "create-repo-file",
|
||||
"title": "在代码仓库中创建文件",
|
||||
"summary": "在代码仓库中创建文件, encoding 可选,默认 raw",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 删除一个代码仓库
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.name - {string} 代码仓库名称
|
||||
*/
|
||||
"delete-repo": {
|
||||
"path": "cnb",
|
||||
"key": "delete-repo",
|
||||
"description": "删除代码仓库, 参数name",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"opencode"
|
||||
],
|
||||
"args": {
|
||||
"name": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "代码仓库名称"
|
||||
}
|
||||
},
|
||||
"skill": "delete-repo",
|
||||
"title": "删除代码仓库",
|
||||
"summary": "删除一个代码仓库",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 批量删除已停止的cnb工作空间,释放资源
|
||||
*/
|
||||
"clean-closed-workspace": {
|
||||
"path": "cnb",
|
||||
"key": "clean-closed-workspace",
|
||||
"description": "批量删除已停止的cnb工作空间",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"opencode"
|
||||
],
|
||||
"args": {},
|
||||
"skill": "clean-closed-workspace",
|
||||
"title": "清理已关闭的cnb工作空间",
|
||||
"summary": "批量删除已停止的cnb工作空间,释放资源",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 保持工作空间存活技能,参数repo:代码仓库路径,例如 user/repo,pipelineId:流水线ID,例如 cnb-708-1ji9sog7o-001
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.repo - {string} 代码仓库路径,例如 user/repo
|
||||
* @param data.pipelineId - {string} 流水线ID,例如 cnb-708-1ji9sog7o-001
|
||||
*/
|
||||
"keep-workspace-alive": {
|
||||
"path": "cnb",
|
||||
"key": "keep-workspace-alive",
|
||||
"description": "保持工作空间存活技能,参数repo:代码仓库路径,例如 user/repo,pipelineId:流水线ID,例如 cnb-708-1ji9sog7o-001",
|
||||
"metadata": {
|
||||
"tags": [],
|
||||
"args": {
|
||||
"repo": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "代码仓库路径,例如 user/repo"
|
||||
},
|
||||
"pipelineId": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "流水线ID,例如 cnb-708-1ji9sog7o-001"
|
||||
}
|
||||
},
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 停止保持工作空间存活技能, 参数repo:代码仓库路径,例如 user/repo,pipelineId:流水线ID,例如 cnb-708-1ji9sog7o-001
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.repo - {string} 代码仓库路径,例如 user/repo
|
||||
* @param data.pipelineId - {string} 流水线ID,例如 cnb-708-1ji9sog7o-001
|
||||
*/
|
||||
"stop-keep-workspace-alive": {
|
||||
"path": "cnb",
|
||||
"key": "stop-keep-workspace-alive",
|
||||
"description": "停止保持工作空间存活技能, 参数repo:代码仓库路径,例如 user/repo,pipelineId:流水线ID,例如 cnb-708-1ji9sog7o-001",
|
||||
"metadata": {
|
||||
"tags": [],
|
||||
"args": {
|
||||
"repo": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "代码仓库路径,例如 user/repo"
|
||||
},
|
||||
"pipelineId": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "流水线ID,例如 cnb-708-1ji9sog7o-001"
|
||||
}
|
||||
},
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 保持当前工作空间存活,防止被关闭或释放资源
|
||||
*/
|
||||
"keep-alive-current-workspace": {
|
||||
"path": "cnb",
|
||||
"key": "keep-alive-current-workspace",
|
||||
"description": "保持当前工作空间存活技能",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"opencode"
|
||||
],
|
||||
"skill": "keep-alive-current-workspace",
|
||||
"title": "保持当前工作空间存活",
|
||||
"summary": "保持当前工作空间存活,防止被关闭或释放资源",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 启动cnb工作空间
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.repo - {string} 代码仓库路径,例如 user/repo
|
||||
* @param data.branch - {string} 分支名称,默认主分支
|
||||
* @param data.ref - {string} 提交引用,例如 commit sha
|
||||
*/
|
||||
"start-workspace": {
|
||||
"path": "cnb",
|
||||
"key": "start-workspace",
|
||||
"description": "启动开发工作空间, 参数 repo",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"opencode"
|
||||
],
|
||||
"args": {
|
||||
"repo": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "代码仓库路径,例如 user/repo"
|
||||
},
|
||||
"branch": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "分支名称,默认主分支",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"ref": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "提交引用,例如 commit sha",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"skill": "start-workspace",
|
||||
"title": "启动cnb工作空间",
|
||||
"summary": "启动cnb工作空间",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 列出cnb工作空间列表,支持按状态过滤, status 可选值 running 或 closed
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.status - {string} 开发环境状态,running: 运行中,closed: 已关闭和停止的
|
||||
* @param data.page - {number} 分页页码,默认 1
|
||||
* @param data.pageSize - {number} 分页大小,默认 20,最大 100
|
||||
* @param data.slug - {string} 仓库路径,例如 groupname/reponame
|
||||
* @param data.branch - {string} 分支名称
|
||||
*/
|
||||
"list-workspace": {
|
||||
"path": "cnb",
|
||||
"key": "list-workspace",
|
||||
"description": "获取cnb开发工作空间列表,可选参数 status=running 获取运行中的环境",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"opencode"
|
||||
],
|
||||
"args": {
|
||||
"status": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "开发环境状态,running: 运行中,closed: 已关闭和停止的",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"page": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "分页页码,默认 1",
|
||||
"type": "number",
|
||||
"optional": true
|
||||
},
|
||||
"pageSize": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "分页大小,默认 20,最大 100",
|
||||
"type": "number",
|
||||
"optional": true
|
||||
},
|
||||
"slug": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "仓库路径,例如 groupname/reponame",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"branch": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "分支名称",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"skill": "list-workspace",
|
||||
"title": "列出cnb工作空间",
|
||||
"summary": "列出cnb工作空间列表,支持按状态过滤, status 可选值 running 或 closed",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 获取工作空间详细信息
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.repo - {string} 代码仓库路径,例如 user/repo
|
||||
* @param data.sn - {string} 工作空间流水线的 sn
|
||||
*/
|
||||
"get-workspace": {
|
||||
"path": "cnb",
|
||||
"key": "get-workspace",
|
||||
"description": "获取工作空间详情,通过 repo 和 sn 获取",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"opencode"
|
||||
],
|
||||
"args": {
|
||||
"repo": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "代码仓库路径,例如 user/repo"
|
||||
},
|
||||
"sn": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "工作空间流水线的 sn"
|
||||
}
|
||||
},
|
||||
"skill": "get-workspace",
|
||||
"title": "获取工作空间详情",
|
||||
"summary": "获取工作空间详细信息",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 删除工作空间,pipelineId 和 sn 二选一
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.pipelineId - {string} 流水线 ID,优先使用
|
||||
* @param data.sn - {string} 流水线构建号
|
||||
* @param data.sns - {array} 批量流水线构建号
|
||||
*/
|
||||
"delete-workspace": {
|
||||
"path": "cnb",
|
||||
"key": "delete-workspace",
|
||||
"description": "删除工作空间,通过 pipelineId 或 sn",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"opencode"
|
||||
],
|
||||
"args": {
|
||||
"pipelineId": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "流水线 ID,优先使用",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"sn": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "流水线构建号",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"sns": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "批量流水线构建号",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"skill": "delete-workspace",
|
||||
"title": "删除工作空间",
|
||||
"summary": "删除工作空间,pipelineId 和 sn 二选一",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 停止运行中的工作空间
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.pipelineId - {string} 流水线 ID,优先使用
|
||||
* @param data.sn - {string} 流水线构建号
|
||||
*/
|
||||
"stop-workspace": {
|
||||
"path": "cnb",
|
||||
"key": "stop-workspace",
|
||||
"description": "停止工作空间,通过 pipelineId 或 sn",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"opencode"
|
||||
],
|
||||
"args": {
|
||||
"pipelineId": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "流水线 ID,优先使用",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"sn": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "流水线构建号",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"skill": "stop-workspace",
|
||||
"title": "停止工作空间",
|
||||
"summary": "停止运行中的工作空间",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 获取当前cnb工作空间的port代理uri,用于端口转发
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.port - {number} 端口号,默认为51515
|
||||
*/
|
||||
"get-cnb-port-uri": {
|
||||
"path": "cnb",
|
||||
"key": "get-cnb-port-uri",
|
||||
"description": "获取当前cnb工作空间的port代理uri",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"opencode"
|
||||
],
|
||||
"args": {
|
||||
"port": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "端口号,默认为51515",
|
||||
"type": "number",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"skill": "get-cnb-port-uri",
|
||||
"title": "获取当前cnb工作空间的port代理uri",
|
||||
"summary": "获取当前cnb工作空间的port代理uri,用于端口转发",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 获取当前cnb工作空间的vscode代理uri,用于在浏览器中访问vscode,包含多种访问方式,如web、vscode、codebuddy、cursor、ssh
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.web - {boolean} 是否获取vscode web的访问uri,默认为false
|
||||
* @param data.vscode - {boolean} 是否获取vscode的代理uri,默认为true
|
||||
* @param data.codebuddy - {boolean} 是否获取codebuddy的代理uri,默认为false
|
||||
* @param data.cursor - {boolean} 是否获取cursor的代理uri,默认为false
|
||||
* @param data.ssh - {boolean} 是否获取vscode remote ssh的连接字符串,默认为false
|
||||
*/
|
||||
"get-cnb-vscode-uri": {
|
||||
"path": "cnb",
|
||||
"key": "get-cnb-vscode-uri",
|
||||
"description": "获取当前cnb工作空间的vscode代理uri, 包括多种访问方式, 如web、vscode、codebuddy、cursor、ssh",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"opencode"
|
||||
],
|
||||
"args": {
|
||||
"web": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "是否获取vscode web的访问uri,默认为false",
|
||||
"type": "boolean",
|
||||
"optional": true
|
||||
},
|
||||
"vscode": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "是否获取vscode的代理uri,默认为true",
|
||||
"type": "boolean",
|
||||
"optional": true
|
||||
},
|
||||
"codebuddy": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "是否获取codebuddy的代理uri,默认为false",
|
||||
"type": "boolean",
|
||||
"optional": true
|
||||
},
|
||||
"cursor": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "是否获取cursor的代理uri,默认为false",
|
||||
"type": "boolean",
|
||||
"optional": true
|
||||
},
|
||||
"ssh": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "是否获取vscode remote ssh的连接字符串,默认为false",
|
||||
"type": "boolean",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"skill": "get-cnb-vscode-uri",
|
||||
"title": "获取当前cnb工作空间的编辑器访问地址",
|
||||
"summary": "获取当前cnb工作空间的vscode代理uri,用于在浏览器中访问vscode,包含多种访问方式,如web、vscode、codebuddy、cursor、ssh",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 设置当前cnb工作空间的cookie环境变量,用于界面操作定制模块功能,例子:CNBSESSION=xxxx;csrfkey=2222xxxx;
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.cookie - {string} cnb的cookie值
|
||||
*/
|
||||
"set-cnb-cookie": {
|
||||
"path": "cnb",
|
||||
"key": "set-cnb-cookie",
|
||||
"description": "设置当前cnb工作空间的cookie环境变量",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"opencode"
|
||||
],
|
||||
"args": {
|
||||
"cookie": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "cnb的cookie值"
|
||||
}
|
||||
},
|
||||
"skill": "set-cnb-cookie",
|
||||
"title": "设置当前cnb工作空间的cookie环境变量",
|
||||
"summary": "设置当前cnb工作空间的cookie环境变量,用于界面操作定制模块功能,例子:CNBSESSION=xxxx;csrfkey=2222xxxx;",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 获取当前cnb工作空间的cookie环境变量,用于界面操作定制模块功能
|
||||
*/
|
||||
"get-cnb-cookie": {
|
||||
"path": "cnb",
|
||||
"key": "get-cnb-cookie",
|
||||
"description": "获取当前cnb工作空间的cookie环境变量",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"opencode"
|
||||
],
|
||||
"args": {},
|
||||
"skill": "get-cnb-cookie",
|
||||
"title": "获取当前cnb工作空间的cookie环境变量",
|
||||
"summary": "获取当前cnb工作空间的cookie环境变量,用于界面操作定制模块功能",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 调用cnb的知识库ai对话功能进行聊天,基于cnb提供的ai能力
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.question - {string} 用户输入的消息内容
|
||||
* @param data.repo - {string} 知识库仓库ID,默认为空表示使用默认知识库
|
||||
*/
|
||||
"cnb-ai-chat": {
|
||||
"path": "cnb",
|
||||
"key": "cnb-ai-chat",
|
||||
"description": "调用cnb的知识库ai对话功能进行聊天",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"opencode"
|
||||
],
|
||||
"args": {
|
||||
"question": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "用户输入的消息内容"
|
||||
},
|
||||
"repo": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "知识库仓库ID,默认为空表示使用默认知识库",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"skill": "cnb-ai-chat",
|
||||
"title": "调用cnb的知识库ai对话功能进行聊天",
|
||||
"summary": "调用cnb的知识库ai对话功能进行聊天,基于cnb提供的ai能力",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 调用cnb的知识库RAG查询功能进行问答,基于cnb提供的知识库能力
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.question - {string} 用户输入的消息内容
|
||||
* @param data.repo - {string} 知识库仓库ID,默认为空表示使用默认知识库
|
||||
*/
|
||||
"cnb-rag-query": {
|
||||
"path": "cnb",
|
||||
"key": "cnb-rag-query",
|
||||
"description": "调用cnb的知识库RAG查询功能进行问答",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"opencode"
|
||||
],
|
||||
"args": {
|
||||
"question": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "用户输入的消息内容"
|
||||
},
|
||||
"repo": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "知识库仓库ID,默认为空表示使用默认知识库",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"skill": "cnb-rag-query",
|
||||
"title": "调用cnb的知识库RAG查询功能进行问答",
|
||||
"summary": "调用cnb的知识库RAG查询功能进行问答,基于cnb提供的知识库能力",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 查询 Issue 列表
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.repo - {string} 代码仓库名称, 如 my-user/my-repo
|
||||
* @param data.state - {string} Issue 状态:open 或 closed
|
||||
* @param data.keyword - {string} 问题搜索关键词
|
||||
* @param data.labels - {string} 问题标签,多个用逗号分隔
|
||||
* @param data.page - {number} 分页页码,默认: 1
|
||||
* @param data.page_size - {number} 分页每页大小,默认: 30
|
||||
* @param data.order_by - {string} 排序方式,如 created_at, -updated_at
|
||||
*/
|
||||
"list-issues": {
|
||||
"path": "cnb",
|
||||
"key": "list-issues",
|
||||
"description": "查询 Issue 列表, 参数 repo, state, keyword, labels, page, page_size 等",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"opencode"
|
||||
],
|
||||
"args": {
|
||||
"repo": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "代码仓库名称, 如 my-user/my-repo"
|
||||
},
|
||||
"state": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "Issue 状态:open 或 closed",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"keyword": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "问题搜索关键词",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"labels": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "问题标签,多个用逗号分隔",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"page": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "分页页码,默认: 1",
|
||||
"type": "number",
|
||||
"optional": true
|
||||
},
|
||||
"page_size": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "分页每页大小,默认: 30",
|
||||
"type": "number",
|
||||
"optional": true
|
||||
},
|
||||
"order_by": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "排序方式,如 created_at, -updated_at",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"skill": "list-issues",
|
||||
"title": "查询 Issue 列表",
|
||||
"summary": "查询 Issue 列表",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 创建一个新的 Issue
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.repo - {string} 代码仓库名称, 如 my-user/my-repo
|
||||
* @param data.title - {string} Issue 标题
|
||||
* @param data.body - {string} Issue 描述内容
|
||||
* @param data.assignees - {array} 指派人列表
|
||||
* @param data.labels - {array} 标签列表
|
||||
* @param data.priority - {string} 优先级
|
||||
*/
|
||||
"create-issue": {
|
||||
"path": "cnb",
|
||||
"key": "create-issue",
|
||||
"description": "创建 Issue, 参数 repo, title, body, assignees, labels, priority",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"opencode"
|
||||
],
|
||||
"args": {
|
||||
"repo": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "代码仓库名称, 如 my-user/my-repo"
|
||||
},
|
||||
"title": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "Issue 标题"
|
||||
},
|
||||
"body": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "Issue 描述内容",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"assignees": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "指派人列表",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"optional": true
|
||||
},
|
||||
"labels": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "标签列表",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"optional": true
|
||||
},
|
||||
"priority": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "优先级",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"skill": "create-issue",
|
||||
"title": "创建 Issue",
|
||||
"summary": "创建一个新的 Issue",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 完成一个 Issue(将 state 改为 closed)
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.repo - {string} 代码仓库名称, 如 my-user/my-repo
|
||||
* @param data.issueNumber - {unknown} Issue 编号
|
||||
* @param data.state - {string} Issue 状态,默认为 closed
|
||||
*/
|
||||
"complete-issue": {
|
||||
"path": "cnb",
|
||||
"key": "complete-issue",
|
||||
"description": "完成 Issue, 参数 repo, issueNumber",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"opencode"
|
||||
],
|
||||
"args": {
|
||||
"repo": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "代码仓库名称, 如 my-user/my-repo"
|
||||
},
|
||||
"issueNumber": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
}
|
||||
],
|
||||
"description": "Issue 编号"
|
||||
},
|
||||
"state": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "Issue 状态,默认为 closed",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"skill": "complete-issue",
|
||||
"title": "完成 CNB的任务Issue",
|
||||
"summary": "完成一个 Issue(将 state 改为 closed)",
|
||||
"url": "/root/v1/dev-cnb",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
}
|
||||
}
|
||||
} as const;
|
||||
const queryApi = createQueryApi({ api, query });
|
||||
|
||||
export { queryApi };
|
||||
95
src/pages/config/gitea/page.tsx
Normal file
95
src/pages/config/gitea/page.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { useGiteaConfigStore } from './store';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { giteaConfigSchema } from './store/schema';
|
||||
import { toast } from 'sonner';
|
||||
import { useLayoutStore } from '../../auth/store';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
|
||||
export const GiteaConfigPage = () => {
|
||||
const { config, setConfig, resetConfig, saveToRemote, loadFromRemote, checkConfig } = useGiteaConfigStore();
|
||||
const layoutStore = useLayoutStore(useShallow(state => ({ me: state.me })))
|
||||
|
||||
useEffect(() => {
|
||||
if (layoutStore.me) {
|
||||
checkConfig({ isUser: !!layoutStore.me, reload: true })
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const result = giteaConfigSchema.safeParse(config);
|
||||
if (result.success) {
|
||||
toast.success('Gitea 配置已保存');
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 400);
|
||||
} else {
|
||||
console.error('验证错误:', result.error.format());
|
||||
toast.error('配置验证失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (field: keyof typeof config, value: string) => {
|
||||
setConfig({ [field]: value });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-2xl py-4 md:py-8 px-4">
|
||||
<div className="bg-white md:rounded-lg md:border md:shadow-sm">
|
||||
<div className="p-4 md:p-6 border-b">
|
||||
<h1 className="text-xl md:text-2xl font-bold">Gitea 配置</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
配置您的 Gitea API 设置。这些设置会保存在浏览器的本地存储中。
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 md:p-6">
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="gitea-url">Gitea 地址</Label>
|
||||
<Input
|
||||
id="gitea-url"
|
||||
type="url"
|
||||
value={config.GITEA_URL}
|
||||
onChange={(e) => handleChange('GITEA_URL', e.target.value)}
|
||||
placeholder="https://git.xiongxiao.me"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="gitea-token">Gitea Token</Label>
|
||||
<Input
|
||||
id="gitea-token"
|
||||
type="password"
|
||||
value={config.GITEA_TOKEN}
|
||||
onChange={(e) => handleChange('GITEA_TOKEN', e.target.value)}
|
||||
placeholder="请输入您的 Gitea Access Token"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-2 sm:gap-4">
|
||||
<Button type="submit" className="w-full sm:w-auto">保存配置</Button>
|
||||
<Button type="button" variant="outline" onClick={resetConfig} className="w-full sm:w-auto">
|
||||
重置为默认值
|
||||
</Button>
|
||||
{layoutStore.me && <>
|
||||
<Button type="button" variant="outline" onClick={loadFromRemote} className="w-full sm:w-auto">
|
||||
获取远端配置
|
||||
</Button>
|
||||
<Button type="button" variant="outline" onClick={saveToRemote} className="w-full sm:w-auto">
|
||||
保存到远端
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GiteaConfigPage;
|
||||
103
src/pages/config/gitea/store/index.ts
Normal file
103
src/pages/config/gitea/store/index.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { create } from 'zustand';
|
||||
import type { GiteaConfig } from './schema';
|
||||
import { queryLogin } from '@/modules/query';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
type GiteaConfigState = {
|
||||
config: GiteaConfig;
|
||||
setConfig: (config: Partial<GiteaConfig>) => void;
|
||||
resetConfig: () => void;
|
||||
saveToRemote: () => Promise<void>;
|
||||
loadFromRemote: () => Promise<boolean>;
|
||||
checkConfig: (opts?: { isUser?: boolean, reload?: boolean }) => Promise<boolean>;
|
||||
};
|
||||
|
||||
const DEFAULT_CONFIG: GiteaConfig = {
|
||||
GITEA_TOKEN: '',
|
||||
GITEA_URL: 'https://git.xiongxiao.me',
|
||||
};
|
||||
|
||||
const loadInitialConfig = (): GiteaConfig => {
|
||||
try {
|
||||
const token = localStorage.getItem('GITEA_TOKEN') || '';
|
||||
const url = localStorage.getItem('GITEA_URL') || DEFAULT_CONFIG.GITEA_URL;
|
||||
return {
|
||||
GITEA_TOKEN: token,
|
||||
GITEA_URL: url,
|
||||
};
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
return DEFAULT_CONFIG;
|
||||
};
|
||||
|
||||
const saveConfig = (config: GiteaConfig) => {
|
||||
try {
|
||||
localStorage.setItem('GITEA_TOKEN', config.GITEA_TOKEN);
|
||||
localStorage.setItem('GITEA_URL', config.GITEA_URL);
|
||||
} catch (error) {
|
||||
console.error('Failed to save config:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const useGiteaConfigStore = create<GiteaConfigState>()((set, get) => ({
|
||||
config: loadInitialConfig(),
|
||||
setConfig: (newConfig) =>
|
||||
set((state) => {
|
||||
const updatedConfig = { ...state.config, ...newConfig };
|
||||
saveConfig(updatedConfig);
|
||||
return { config: updatedConfig };
|
||||
}),
|
||||
resetConfig: () => {
|
||||
saveConfig(DEFAULT_CONFIG);
|
||||
return set({ config: DEFAULT_CONFIG });
|
||||
},
|
||||
saveToRemote: async () => {
|
||||
const _config = get().config;
|
||||
const res = await queryLogin.post({
|
||||
path: 'config',
|
||||
key: 'update',
|
||||
data: {
|
||||
key: 'gitea_config.json',
|
||||
data: _config,
|
||||
}
|
||||
});
|
||||
if (res.code === 200) {
|
||||
toast.success('保存到远端成功')
|
||||
} else {
|
||||
toast.error('保存到远端失败')
|
||||
}
|
||||
},
|
||||
loadFromRemote: async () => {
|
||||
const setConfig = (config: GiteaConfig) => set({ config });
|
||||
const res = await queryLogin.post({
|
||||
path: 'config',
|
||||
key: 'get',
|
||||
data: {
|
||||
key: 'gitea_config.json',
|
||||
}
|
||||
})
|
||||
if (res.code === 404) {
|
||||
toast.error('远端配置不存在')
|
||||
return false;
|
||||
}
|
||||
if (res.code === 200) {
|
||||
const config = res.data?.data as GiteaConfig;
|
||||
setConfig(config);
|
||||
toast.success('获取远端配置成功')
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
checkConfig: async (opts?: { isUser?: boolean, reload?: boolean }) => {
|
||||
const { GITEA_TOKEN } = get().config;
|
||||
if (!GITEA_TOKEN && opts?.isUser) {
|
||||
const res = await get().loadFromRemote();
|
||||
if (opts?.reload && res) {
|
||||
location.reload();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
return false
|
||||
}
|
||||
}));
|
||||
13
src/pages/config/gitea/store/schema.ts
Normal file
13
src/pages/config/gitea/store/schema.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const giteaConfigSchema = z.object({
|
||||
GITEA_TOKEN: z.string().min(1, 'Gitea Token is required'),
|
||||
GITEA_URL: z.url('Must be a valid URL'),
|
||||
});
|
||||
|
||||
export type GiteaConfig = z.infer<typeof giteaConfigSchema>;
|
||||
|
||||
export const defaultGiteaConfig: GiteaConfig = {
|
||||
GITEA_TOKEN: '',
|
||||
GITEA_URL: 'https://git.xiongxiao.me',
|
||||
};
|
||||
16
src/pages/config/hooks/check.ts
Normal file
16
src/pages/config/hooks/check.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useEffect } from "react";
|
||||
import { useConfigStore } from "../store";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
export const useCheckConfig = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const config = useConfigStore.getState().config;
|
||||
if (!config.CNB_API_KEY) {
|
||||
navigate({
|
||||
to: '/config'
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
109
src/pages/config/page.tsx
Normal file
109
src/pages/config/page.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { useConfigStore } from './store';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { Info } from 'lucide-react';
|
||||
import { SidebarLayout } from '../sidebar/components';
|
||||
export const ConfigPage = () => {
|
||||
const { config, setConfig, saveToRemote, loadFromRemote } = useConfigStore();
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
saveToRemote();
|
||||
};
|
||||
|
||||
const handleChange = (field: keyof typeof config, value: string | boolean) => {
|
||||
setConfig({ [field]: value });
|
||||
};
|
||||
|
||||
return (
|
||||
<SidebarLayout>
|
||||
<div className="container mx-auto max-w-2xl py-4 md:py-8 px-4">
|
||||
<div className="bg-white md:rounded-lg md:border md:shadow-sm">
|
||||
<div className="p-4 md:p-6 border-b">
|
||||
<h1 className="text-xl md:text-2xl font-bold">CNB 配置</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
配置您的 CNB API 设置。
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 md:p-6">
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="api-key">API 密钥</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Info className="h-4 w-4 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
用于访问 CNB API 的密钥。
|
||||
<a
|
||||
href="https://cnb.cool/profile/token"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline ml-1 hover:text-blue-400"
|
||||
>
|
||||
点击获取
|
||||
</a>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Input
|
||||
id="api-key"
|
||||
type="text"
|
||||
value={config.CNB_API_KEY}
|
||||
onChange={(e) => handleChange('CNB_API_KEY', e.target.value)}
|
||||
placeholder="请输入您的 CNB API 密钥"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="cookie">Cookie</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Info className="h-4 w-4 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
用于身份验证的 Cookie 信息,有效期7天。
|
||||
<a
|
||||
href="https://cnb.cool/kevisual/cnb-live-extension"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline ml-1 hover:text-blue-400"
|
||||
>
|
||||
前往获取
|
||||
</a>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Input
|
||||
id="cookie"
|
||||
type="text"
|
||||
value={config.CNB_COOKIE}
|
||||
onChange={(e) => handleChange('CNB_COOKIE', e.target.value)}
|
||||
placeholder="请输入您的 CNB Cookie"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-2 sm:gap-4">
|
||||
<Button type="button" variant="outline" onClick={loadFromRemote} className="w-full sm:w-auto">
|
||||
获取远端配置
|
||||
</Button>
|
||||
<Button type="button" variant="outline" onClick={saveToRemote} className="w-full sm:w-auto">
|
||||
保存到远端
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfigPage;
|
||||
99
src/pages/config/store/index.ts
Normal file
99
src/pages/config/store/index.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import type { Config, } from './schema';
|
||||
import { queryLogin } from '@/modules/query';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
type ConfigState = {
|
||||
config: Config;
|
||||
setConfig: (config: Partial<Config>) => void;
|
||||
resetConfig: () => void;
|
||||
saveToRemote: () => Promise<void>;
|
||||
loadFromRemote: () => Promise<boolean>;
|
||||
checkConfig: (opts?: { isUser?: boolean, reload?: boolean }) => Promise<boolean>;
|
||||
};
|
||||
|
||||
const STORAGE_KEY = 'cnb-config';
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
CNB_API_KEY: '',
|
||||
CNB_COOKIE: '',
|
||||
}
|
||||
const loadInitialConfig = (): Config => {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
return JSON.parse(stored);
|
||||
}
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
return DEFAULT_CONFIG;
|
||||
};
|
||||
|
||||
export const useConfigStore = create<ConfigState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
config: loadInitialConfig(),
|
||||
setConfig: (newConfig) =>
|
||||
set((state) => ({
|
||||
config: { ...state.config, ...newConfig },
|
||||
})),
|
||||
resetConfig: () =>
|
||||
set({
|
||||
config: DEFAULT_CONFIG,
|
||||
}),
|
||||
saveToRemote: async () => {
|
||||
const _config = get().config;
|
||||
const res = await queryLogin.post({
|
||||
path: 'config',
|
||||
key: 'update',
|
||||
data: {
|
||||
key: 'cnb_center_config.json',
|
||||
data: _config,
|
||||
}
|
||||
});
|
||||
if (res.code === 200) {
|
||||
toast.success('保存到远端成功')
|
||||
} else {
|
||||
toast.error('保存到远端失败')
|
||||
}
|
||||
},
|
||||
loadFromRemote: async () => {
|
||||
const setConfig = (config: Config) => set({ config });
|
||||
const res = await queryLogin.post({
|
||||
path: 'config',
|
||||
key: 'get',
|
||||
data: {
|
||||
key: 'cnb_center_config.json',
|
||||
}
|
||||
})
|
||||
if (res.code === 404) {
|
||||
toast.error('远端配置不存在')
|
||||
return false;
|
||||
}
|
||||
if (res.code === 200) {
|
||||
const config = res.data?.data as typeof config;
|
||||
setConfig(config);
|
||||
toast.success('获取远端配置成功')
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
checkConfig: async (opts?: { isUser?: boolean, reload?: boolean }) => {
|
||||
const { CNB_API_KEY } = get().config;
|
||||
if (!CNB_API_KEY && opts?.isUser) {
|
||||
const res = await get().loadFromRemote();
|
||||
if (opts?.reload && res) {
|
||||
location.reload();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
return false
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: STORAGE_KEY,
|
||||
}
|
||||
)
|
||||
);
|
||||
13
src/pages/config/store/schema.ts
Normal file
13
src/pages/config/store/schema.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const configSchema = z.object({
|
||||
CNB_API_KEY: z.string().min(1, 'API Key is required'),
|
||||
CNB_COOKIE: z.string().min(1, 'Cookie is required'),
|
||||
});
|
||||
|
||||
export type Config = z.infer<typeof configSchema>;
|
||||
|
||||
export const defaultConfig: Config = {
|
||||
CNB_API_KEY: '',
|
||||
CNB_COOKIE: '',
|
||||
};
|
||||
8
src/pages/demo/page.tsx
Normal file
8
src/pages/demo/page.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { useDemoStore } from './store/index'
|
||||
export const App = () => {
|
||||
const demoStore = useDemoStore()
|
||||
console.log('demo', demoStore.formData)
|
||||
return <div>App</div>
|
||||
}
|
||||
|
||||
export default App;
|
||||
95
src/pages/demo/store/index.ts
Normal file
95
src/pages/demo/store/index.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { create } from 'zustand';
|
||||
import { query } from '@/modules/query';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface Data {
|
||||
id: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
type State = {
|
||||
formData: Record<string, any>;
|
||||
setFormData: (data: Record<string, any>) => void;
|
||||
showEdit: boolean;
|
||||
setShowEdit: (showEdit: boolean) => void;
|
||||
loading: boolean;
|
||||
setLoading: (loading: boolean) => void;
|
||||
list: Data[];
|
||||
getItem: (id: string) => Promise<any>;
|
||||
getList: () => Promise<any>;
|
||||
updateData: (data: Data) => Promise<void>;
|
||||
deleteData: (id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const useDemoStore = create<State>((set, get) => {
|
||||
return {
|
||||
formData: {},
|
||||
setFormData: (data) => set({ formData: data }),
|
||||
showEdit: false,
|
||||
setShowEdit: (showEdit) => set({ showEdit }),
|
||||
loading: false,
|
||||
setLoading: (loading) => set({ loading }),
|
||||
list: [],
|
||||
getItem: async (id) => {
|
||||
const { setLoading } = get();
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await query.post({
|
||||
path: 'demo',
|
||||
key: 'item',
|
||||
data: { id }
|
||||
})
|
||||
if (res.code === 200) {
|
||||
return res;
|
||||
} else {
|
||||
toast.error(res.message || '请求失败');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
getList: async () => {
|
||||
const { setLoading } = get();
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await query.post({
|
||||
path: 'demo',
|
||||
key: 'list'
|
||||
});
|
||||
if (res.code === 200) {
|
||||
const list = res.data?.list || []
|
||||
set({ list });
|
||||
} else {
|
||||
toast.error(res.message || '请求失败');
|
||||
}
|
||||
return res;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
updateData: async (data) => {
|
||||
const res = await query.post({
|
||||
path: 'demo',
|
||||
key: 'update',
|
||||
data
|
||||
})
|
||||
if (res.code === 200) {
|
||||
get().getList()
|
||||
} else {
|
||||
toast.error(res.message || '请求失败');
|
||||
}
|
||||
},
|
||||
deleteData: async (id) => {
|
||||
const res = await query.post({
|
||||
path: 'demo',
|
||||
key: 'delete',
|
||||
data: { id }
|
||||
})
|
||||
if (res.code === 200) {
|
||||
get().getList()
|
||||
} else {
|
||||
toast.error(res.message || '请求失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
2
src/pages/page.tsx
Normal file
2
src/pages/page.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
import App from './repos/page'
|
||||
export default App
|
||||
141
src/pages/repos/components/BuildConfig.tsx
Normal file
141
src/pages/repos/components/BuildConfig.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRepoStore } from "../store";
|
||||
import { useShallow } from "zustand/shallow";
|
||||
import CodeMirror from "@uiw/react-codemirror";
|
||||
import { yaml } from "@codemirror/lang-yaml";
|
||||
import { useLayoutStore } from "@/pages/auth/store";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ArrowLeft, Workflow } from "lucide-react";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
export const BuildConfig = () => {
|
||||
const repoStore = useRepoStore(useShallow((state) => ({
|
||||
getItem: state.getItem,
|
||||
editRepo: state.editRepo,
|
||||
buildConfig: state.buildConfig,
|
||||
setBuildConfig: state.setBuildConfig,
|
||||
initBuildConfig: state.initBuildConfig,
|
||||
deleteBuildConfig: state.deleteBuildConfig,
|
||||
loading: state.loading,
|
||||
buildWorkspace: state.buildWorkspace,
|
||||
})));
|
||||
const repo = repoStore.editRepo!;
|
||||
const navigate = useNavigate();
|
||||
const me = useLayoutStore((state) => state.me);
|
||||
const [localConfig, setLocalConfig] = useState(repoStore.buildConfig?.config || "");
|
||||
const [mounted, setMounted] = useState(false);
|
||||
// 同步 buildConfig 变化时的状态
|
||||
useEffect(() => {
|
||||
setLocalConfig(repoStore.buildConfig?.config || "");
|
||||
}, [repoStore.buildConfig]);
|
||||
useEffect(() => {
|
||||
if (repo) {
|
||||
repoStore.initBuildConfig({ repo: repo, user: me }).then(() => {
|
||||
setMounted(true);
|
||||
});
|
||||
} else {
|
||||
setMounted(true);
|
||||
}
|
||||
}, [repo])
|
||||
|
||||
const handleSave = () => {
|
||||
if (repoStore.buildConfig) {
|
||||
repoStore.setBuildConfig({
|
||||
...repoStore.buildConfig,
|
||||
config: localConfig,
|
||||
}, true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFieldChange = (field: string, value: string | null) => {
|
||||
if (repoStore.buildConfig && value !== null) {
|
||||
repoStore.setBuildConfig({
|
||||
...repoStore.buildConfig,
|
||||
[field]: value,
|
||||
}, false);
|
||||
}
|
||||
};
|
||||
if (repoStore.loading || !mounted) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row gap-3 md:gap-4 h-full overflow-hidden">
|
||||
{/* 左侧边栏 - 配置信息 */}
|
||||
<div className="w-full md:w-64 shrink-0 space-y-3 md:space-y-4 order-2 md:order-1">
|
||||
<div className="text-lg md:text-xl font-bold border-b pb-2 mb-3 md:mb-4 flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => navigate({ to: '/' })}
|
||||
className="cursor-pointer flex items-center justify-center w-8 h-8 rounded-md hover:bg-neutral-100 transition-colors shrink-0"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 text-neutral-600" />
|
||||
</button>
|
||||
<span className="text-base md:text-lg font-semibold truncate">构建配置</span>
|
||||
<button
|
||||
onClick={repoStore.buildWorkspace}
|
||||
className="ml-auto p-1.5 md:p-2 text-xs md:text-sm cursor-pointer bg-gray-500 text-white rounded hover:bg-gray-600 flex items-center gap-1"
|
||||
title="构建工作空间"
|
||||
>
|
||||
<Workflow className="w-3 h-3 md:w-4 md:h-4" />
|
||||
<span className="hidden md:inline">构建</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm text-neutral-500">仓库</label>
|
||||
<Input
|
||||
value={repoStore.buildConfig?.repo || ""}
|
||||
onChange={(e) => handleFieldChange("repo", e.target.value)}
|
||||
placeholder="仓库名称"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm text-neutral-500">分支</label>
|
||||
<Input
|
||||
value={repoStore.buildConfig?.branch || ""}
|
||||
onChange={(e) => handleFieldChange("branch", e.target.value)}
|
||||
placeholder="分支名称"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm text-neutral-500">事件</label>
|
||||
<Input
|
||||
value={repoStore.buildConfig?.event || ""}
|
||||
onChange={(e) => handleFieldChange("event", e.target.value)}
|
||||
placeholder="事件名称"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧 - 编辑器 */}
|
||||
<div className="flex-1 flex flex-col h-full order-1 md:order-2 min-h-[300px] md:min-h-0">
|
||||
<div className="flex items-center justify-between mb-2 gap-2">
|
||||
<span className="text-sm font-medium">配置文件</span>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="px-2 md:px-3 cursor-pointer py-1 text-xs md:text-sm bg-primary text-white rounded hover:bg-primary/90"
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
<button
|
||||
onClick={() => repoStore.deleteBuildConfig({ repo: repo, user: me })}
|
||||
className="px-2 md:px-3 cursor-pointer py-1 text-xs md:text-sm bg-red-500 text-white rounded hover:bg-red-600"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border rounded-md flex-1 h-[calc(100%-40px)] overflow-auto scrollbar">
|
||||
<CodeMirror
|
||||
value={localConfig}
|
||||
height="100%"
|
||||
extensions={[yaml()]}
|
||||
onChange={(value) => setLocalConfig(value)}
|
||||
theme="light"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BuildConfig;
|
||||
352
src/pages/repos/components/RepoCard.tsx
Normal file
352
src/pages/repos/components/RepoCard.tsx
Normal file
@@ -0,0 +1,352 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
import { Star, GitFork, FileText, Edit, FolderGit2, MoreVertical, FileText as IssueIcon, Settings, Play, Trash2, RefreshCw, BookOpen, Copy, Clock, Info, Eye, Square, LinkIcon, ExternalLink, ArrowLeft } from 'lucide-react'
|
||||
import { useRepoStore } from '../store'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useShallow } from 'zustand/shallow'
|
||||
import { myOrgs } from '../store/build'
|
||||
import { app } from '@/agents/app'
|
||||
import { toast } from 'sonner'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import clsx from 'clsx'
|
||||
|
||||
interface RepoCardProps {
|
||||
repo: any
|
||||
showReturn?: boolean
|
||||
}
|
||||
|
||||
export function RepoCard({ showReturn = false, repo }: RepoCardProps) {
|
||||
const [deletePopoverOpen, setDeletePopoverOpen] = useState(false)
|
||||
const store = useRepoStore(useShallow((state) => ({
|
||||
workspaceList: state.workspaceList,
|
||||
getWorkspaceDetail: state.getWorkspaceDetail,
|
||||
getList: state.getList,
|
||||
buildUpdate: state.buildUpdate,
|
||||
stopWorkspace: state.stopWorkspace,
|
||||
setSelectedSyncRepo: state.setSelectedSyncRepo,
|
||||
setSyncDialogOpen: state.setSyncDialogOpen,
|
||||
startWorkspace: state.startWorkspace,
|
||||
setEditRepo: state.setEditRepo,
|
||||
setShowEditDialog: state.setShowEditDialog,
|
||||
deleteItem: state.deleteItem,
|
||||
})));
|
||||
const workspace = useMemo(() => {
|
||||
return store.workspaceList.find(ws => ws.slug === repo.path)
|
||||
}, [store.workspaceList, repo.path])
|
||||
const isWorkspaceActive = !!workspace
|
||||
const navigate = useNavigate();
|
||||
const isKnowledge = repo?.flags === "KnowledgeBase"
|
||||
const createKnow = async () => {
|
||||
const res = await app.run({ path: 'cnb', key: 'build-knowledge-base', payload: { repo: repo.path } })
|
||||
if (res.code === 200) {
|
||||
toast.success("知识库创建中")
|
||||
store.getList({}, true)
|
||||
}
|
||||
}
|
||||
const onClone = async () => {
|
||||
const url = `git clone https://cnb.cool/${repo.path}`
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
toast.success('克隆地址已复制到剪贴板')
|
||||
}).catch(() => {
|
||||
toast.error('复制失败')
|
||||
})
|
||||
}
|
||||
const onUpdate = async () => {
|
||||
await store.buildUpdate({ path: repo.path });
|
||||
}
|
||||
const handleIssue = (repo: any) => {
|
||||
window.open(`https://cnb.cool/${repo.path}/-/issues`)
|
||||
}
|
||||
const handleSettings = (repo: any) => {
|
||||
window.open(`https://cnb.cool/${repo.path}/-/settings`)
|
||||
}
|
||||
const openInCNB = (isDetail = true) => {
|
||||
if (!showReturn && isDetail) {
|
||||
navigate({ to: `/repo?repo=${repo.path}` })
|
||||
} else {
|
||||
window.open(`https://cnb.cool/${repo.path}`, '_blank')
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Card className="relative p-0 overflow-hidden border border-neutral-200 bg-white hover:shadow-xl hover:border-neutral-300 transition-all duration-300 group pb-12 md:pb-14">
|
||||
<div className="p-4 md:p-6 space-y-3 md:space-y-4">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
{showReturn && (
|
||||
<button
|
||||
onClick={() => navigate({ to: '/' })}
|
||||
className="cursor-pointer flex items-center justify-center w-8 h-8 rounded-md hover:bg-neutral-100 transition-colors shrink-0"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 text-neutral-600" />
|
||||
</button>
|
||||
)}
|
||||
<div
|
||||
className="text-base md:text-lg font-bold text-neutral-900 hover:text-neutral-600 transition-colors line-clamp-1 group-hover:underline cursor-pointer"
|
||||
onClick={() => {
|
||||
openInCNB()
|
||||
}}
|
||||
>
|
||||
{repo.path}
|
||||
</div>
|
||||
{isKnowledge && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<div className="shrink-0">
|
||||
<BookOpen className="w-5 h-5 text-neutral-700" />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>
|
||||
<p>知识库</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
{isWorkspaceActive && (
|
||||
<span className="relative flex h-2.5 w-2.5 shrink-0">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-green-500"></span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 md:gap-2 shrink-0">
|
||||
{isWorkspaceActive && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
store.stopWorkspace(workspace)
|
||||
}}
|
||||
className="h-8 w-8 p-0 border-neutral-200 hover:border-red-600 hover:bg-red-600 hover:text-white transition-all cursor-pointer"
|
||||
>
|
||||
<Square className="w-4 h-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>
|
||||
<p>停止工作区</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (!isWorkspaceActive) {
|
||||
store.startWorkspace(repo)
|
||||
} else {
|
||||
store.getWorkspaceDetail(workspace)
|
||||
}
|
||||
|
||||
}}
|
||||
className="h-8 w-8 p-0 border-neutral-200 hover:border-neutral-900 hover:bg-neutral-900 hover:text-white transition-all cursor-pointer"
|
||||
>
|
||||
{isWorkspaceActive ? <Eye className="w-4 h-4" /> : <Play className="w-4 h-4" />}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>
|
||||
<p>{isWorkspaceActive ? '查看工作区' : '启动工作区'}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-8 w-8 p-0 border-neutral-200 hover:border-neutral-900 hover:bg-neutral-900 hover:text-white transition-all cursor-pointer"
|
||||
>
|
||||
<MoreVertical className="w-4 h-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
<DropdownMenuItem onClick={() => {
|
||||
window.open(repo.web_url, '_blank')
|
||||
}} className="cursor-pointer">
|
||||
<ExternalLink className="w-4 h-4 mr-2" />
|
||||
访问仓库
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => {
|
||||
store.setEditRepo(repo)
|
||||
store.setShowEditDialog(true)
|
||||
}} className="cursor-pointer">
|
||||
<Edit className="w-4 h-4 mr-2" />
|
||||
编辑
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => {
|
||||
createKnow()
|
||||
}} className="cursor-pointer">
|
||||
<BookOpen className="w-4 h-4 mr-2" />
|
||||
知识库创建
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => {
|
||||
onUpdate()
|
||||
}} className="cursor-pointer">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger className={'flex gap-1 items-center'}>
|
||||
<Clock className="w-4 h-4 mr-2" />
|
||||
更新时间
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="max-w-xs">
|
||||
<p>给仓库添加一个空白 commit 的提交,保证在仓库列表中顺序活跃在前面</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={onClone} className="cursor-pointer">
|
||||
<Copy className="w-4 h-4 mr-2" />
|
||||
Clone URL
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onClick={() => handleIssue(repo)} className="cursor-pointer">
|
||||
<IssueIcon className="w-4 h-4 mr-2" />
|
||||
访问问题
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleSettings(repo)} className="cursor-pointer">
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
访问设置
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
setDeletePopoverOpen(true)
|
||||
}}
|
||||
className="cursor-pointer text-red-600 focus:text-red-600 focus:bg-red-50"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
删除
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Popover open={deletePopoverOpen} onOpenChange={setDeletePopoverOpen}>
|
||||
<PopoverTrigger >
|
||||
<div style={{ display: 'none' }} />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium text-sm">确认删除</h4>
|
||||
<p className="text-sm text-neutral-500">
|
||||
确定要删除仓库 <span className="font-semibold text-neutral-900">{repo.path}</span> 吗?此操作无法撤销。
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setDeletePopoverOpen(false)}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="bg-red-600 text-white border-red-600 hover:bg-red-700 hover:border-red-700"
|
||||
onClick={() => {
|
||||
if (repo.path)
|
||||
store.deleteItem(repo.path)
|
||||
setDeletePopoverOpen(false)
|
||||
}}
|
||||
>
|
||||
确认删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-1.5 md:gap-2">
|
||||
{repo.topics && (<>
|
||||
{
|
||||
repo.topics.split(',').map((topic: string, idx: number) => (
|
||||
<Badge key={idx} variant="outline" className="text-xs border-neutral-300 text-neutral-700 hover:bg-neutral-100 transition-colors">
|
||||
{topic.trim()}
|
||||
</Badge>
|
||||
))
|
||||
}
|
||||
</>
|
||||
)}
|
||||
<Badge variant="outline" className="text-xs border-neutral-300 text-neutral-700 hover:bg-neutral-100 transition-colors">{repo.visibility_level}</Badge>
|
||||
</div>
|
||||
<div className={clsx(!showReturn && "cursor-pointer")} onClick={() => {
|
||||
{ !showReturn && openInCNB(false) }
|
||||
}}>
|
||||
{repo.site && (
|
||||
<div
|
||||
className="text-xs text-neutral-500 hover:text-neutral-900 hover:underline flex transition-colors"
|
||||
onClick={(e) => {
|
||||
window.open(repo.site, '_blank')
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<LinkIcon className="w-4 h-4 shrink-0 mr-2" />
|
||||
<div className='truncate grow'>
|
||||
{repo.site}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{repo.description && (
|
||||
<p className="text-sm text-neutral-600 line-clamp-2 min-h-10">
|
||||
{repo.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<div className="absolute bottom-0 left-0 right-0 flex items-center gap-2 md:gap-4 text-xs text-neutral-500 px-4 md:px-6 py-2 md:py-3 border-t border-neutral-100 bg-neutral-50 overflow-x-auto">
|
||||
<span className="flex items-center gap-1 hover:text-neutral-900 transition-colors whitespace-nowrap">
|
||||
<Star className="w-3.5 h-3.5" />
|
||||
<span className="font-medium">{repo.star_count}</span>
|
||||
</span>
|
||||
<span className="flex items-center gap-1 hover:text-neutral-900 transition-colors whitespace-nowrap">
|
||||
<GitFork className="w-3.5 h-3.5" />
|
||||
<span className="font-medium">{repo.fork_count}</span>
|
||||
</span>
|
||||
<span className="flex items-center gap-1 hover:text-neutral-900 transition-colors whitespace-nowrap">
|
||||
<FileText className="w-3.5 h-3.5" />
|
||||
<span className="font-medium">{repo.open_issue_count}</span>
|
||||
</span>
|
||||
{isWorkspaceActive && <span className="flex items-center gap-1 hover:text-neutral-900 transition-colors cursor-pointer whitespace-nowrap"
|
||||
onClick={() => {
|
||||
store.getWorkspaceDetail(workspace)
|
||||
}}>
|
||||
<Play className="w-3.5 h-3.5" />
|
||||
<span className="font-medium">运行中</span>
|
||||
</span>}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
}
|
||||
153
src/pages/repos/modules/CreateRepoDialog.tsx
Normal file
153
src/pages/repos/modules/CreateRepoDialog.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useForm, Controller } from 'react-hook-form'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { useRepoStore } from '../store'
|
||||
import { useShallow } from 'zustand/shallow'
|
||||
|
||||
interface CreateRepoDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
interface FormData {
|
||||
path: string
|
||||
license: string
|
||||
description: string
|
||||
visibility: string
|
||||
}
|
||||
|
||||
export function CreateRepoDialog({ open, onOpenChange }: CreateRepoDialogProps) {
|
||||
const { createRepo, refresh } = useRepoStore(useShallow((state) => ({
|
||||
createRepo: state.createRepo,
|
||||
refresh: state.refresh,
|
||||
})))
|
||||
const { register, handleSubmit, reset, control } = useForm<FormData>()
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
// 重置表单
|
||||
reset({
|
||||
path: '',
|
||||
license: '',
|
||||
description: '',
|
||||
visibility: 'public'
|
||||
})
|
||||
}
|
||||
}, [open, reset])
|
||||
|
||||
const onSubmit = async (data: FormData) => {
|
||||
setIsSubmitting(true)
|
||||
try {
|
||||
const submitData = {
|
||||
path: data.path.trim(),
|
||||
license: data.license.trim(),
|
||||
description: data.description.trim(),
|
||||
visibility: data.visibility,
|
||||
}
|
||||
|
||||
await createRepo(submitData)
|
||||
onOpenChange(false)
|
||||
refresh()
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="w-[90vw] max-w-lg sm:max-w-[525px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>新建仓库</DialogTitle>
|
||||
<DialogDescription>
|
||||
填写仓库信息以创建新的代码仓库
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="path">仓库路径 *</Label>
|
||||
<Input
|
||||
id="path"
|
||||
placeholder="例如: username/repository"
|
||||
{...register('path', { required: true })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">描述</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
placeholder="简短描述你的仓库..."
|
||||
rows={2}
|
||||
{...register('description')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="visibility">可见性</Label>
|
||||
<Controller
|
||||
name="visibility"
|
||||
control={control}
|
||||
defaultValue="public"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Select onValueChange={onChange} value={value}>
|
||||
<SelectTrigger id="visibility">
|
||||
<SelectValue placeholder="选择可见性" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="public">公开 (public)</SelectItem>
|
||||
<SelectItem value="private">私有 (private)</SelectItem>
|
||||
<SelectItem value="protected">保护 (protected)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="topics">主题标签</Label>
|
||||
<Input
|
||||
id="license"
|
||||
placeholder="例如: MIT, Apache-2.0"
|
||||
{...register('license')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex-col sm:flex-row gap-2 sm:gap-0">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isSubmitting}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting} className="w-full sm:w-auto">
|
||||
{isSubmitting ? '创建中...' : '创建仓库'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
145
src/pages/repos/modules/EditRepoDialog.tsx
Normal file
145
src/pages/repos/modules/EditRepoDialog.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { TagsInput } from '@/components/tags-input'
|
||||
import { useRepoStore } from '../store'
|
||||
import { useShallow } from 'zustand/shallow'
|
||||
|
||||
interface EditRepoDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
repo: {
|
||||
id: string
|
||||
path: string
|
||||
description: string
|
||||
site: string
|
||||
topics: string
|
||||
license: string
|
||||
} | null
|
||||
}
|
||||
|
||||
interface FormData {
|
||||
description: string
|
||||
site: string
|
||||
topics: string
|
||||
license: string
|
||||
}
|
||||
|
||||
export function EditRepoDialog({ open, onOpenChange, repo }: EditRepoDialogProps) {
|
||||
const { updateRepoInfo, getList } = useRepoStore(useShallow((state) => ({
|
||||
updateRepoInfo: state.updateRepoInfo,
|
||||
getList: state.getList,
|
||||
})))
|
||||
const { register, handleSubmit, reset, setValue } = useForm<FormData>()
|
||||
const [tags, setTags] = useState<string[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (repo) {
|
||||
const topicsArray = repo.topics ? repo.topics.split(',').map(t => t.trim()).filter(Boolean) : []
|
||||
setTags(topicsArray)
|
||||
reset({
|
||||
description: repo.description || '',
|
||||
site: repo.site || '',
|
||||
topics: repo.topics || '',
|
||||
license: repo.license || ''
|
||||
})
|
||||
}
|
||||
}, [repo, reset])
|
||||
|
||||
const onSubmit = async (data: FormData) => {
|
||||
if (!repo) return
|
||||
|
||||
await updateRepoInfo({
|
||||
path: repo.path,
|
||||
description: data.description?.trim() || '',
|
||||
site: data.site?.trim() || '',
|
||||
topics: tags as any,
|
||||
license: data.license?.trim() || '',
|
||||
})
|
||||
|
||||
await getList({}, true)
|
||||
onOpenChange(false)
|
||||
}
|
||||
|
||||
if (!repo) return null
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="w-[90vw] max-w-2xl! max-h-[85vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>编辑仓库信息</DialogTitle>
|
||||
<DialogDescription className="text-sm truncate">{repo.path}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4 md:space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">描述</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
{...register('description')}
|
||||
placeholder="输入仓库描述"
|
||||
className="w-full min-h-[80px] md:min-h-[100px]"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="site">网站</Label>
|
||||
<Input
|
||||
id="site"
|
||||
{...register('site')}
|
||||
placeholder="https://example.com"
|
||||
type="url"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="topics">标签</Label>
|
||||
<TagsInput
|
||||
value={tags}
|
||||
onChange={setTags}
|
||||
placeholder="输入标签后按 Enter 或逗号添加"
|
||||
/>
|
||||
<p className="text-xs text-gray-500">按 Enter 或逗号添加标签,点击 × 删除</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="license">许可证</Label>
|
||||
<Input
|
||||
id="license"
|
||||
{...register('license')}
|
||||
placeholder="MIT, Apache-2.0, GPL-3.0 等"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex-col sm:flex-row gap-2 sm:gap-0">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="submit" className="w-full sm:w-auto">
|
||||
保存
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
362
src/pages/repos/modules/WorkspaceDetailDialog.tsx
Normal file
362
src/pages/repos/modules/WorkspaceDetailDialog.tsx
Normal file
@@ -0,0 +1,362 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
TooltipProvider
|
||||
} from '@/components/ui/tooltip'
|
||||
import { useRepoStore } from '../store'
|
||||
import type { WorkspaceOpen } from '../store'
|
||||
import {
|
||||
Code2,
|
||||
Terminal,
|
||||
MousePointer2,
|
||||
Lock,
|
||||
Radio,
|
||||
Zap,
|
||||
Copy,
|
||||
Check,
|
||||
Square,
|
||||
Link,
|
||||
ExternalLink,
|
||||
Wind,
|
||||
Plane,
|
||||
Rocket
|
||||
} from 'lucide-react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { useShallow } from 'zustand/shallow'
|
||||
import clsx from 'clsx'
|
||||
|
||||
type LinkItemKey = keyof WorkspaceOpen;
|
||||
interface LinkItem {
|
||||
key: LinkItemKey
|
||||
label: string
|
||||
icon: React.ReactNode
|
||||
order?: number
|
||||
getUrl: (data: Partial<WorkspaceOpen>) => string | undefined
|
||||
}
|
||||
|
||||
const LinkItem = ({ label, icon, url }: { label: string; icon: React.ReactNode; url?: string }) => {
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
|
||||
const handleClick = () => {
|
||||
if (url?.startsWith?.('ssh') || url?.startsWith?.('cnb')) {
|
||||
copy()
|
||||
return;
|
||||
}
|
||||
if (url && url.includes(':')) {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
}
|
||||
const copy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(url!)
|
||||
setIsCopied(true)
|
||||
toast.success('已复制到剪贴板')
|
||||
setTimeout(() => setIsCopied(false), 2000)
|
||||
} catch (error) {
|
||||
toast.error('复制失败')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider delay={200}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div
|
||||
onClick={handleClick}
|
||||
className="relative flex items-center gap-3 p-3 rounded-lg border border-neutral-200 hover:border-neutral-900 hover:bg-neutral-50 transition-all disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:border-neutral-200 disabled:hover:bg-transparent group"
|
||||
>
|
||||
<div className="w-8 h-8 flex items-center justify-center text-neutral-700">
|
||||
{icon}
|
||||
</div>
|
||||
<span className="text-sm font-medium text-neutral-900 flex-1 text-left truncate">{label}</span>
|
||||
{url && (
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
copy()
|
||||
}}
|
||||
role="button"
|
||||
className="w-6 h-6 flex items-center justify-center text-neutral-500 hover:text-neutral-900 hover:bg-neutral-100 rounded transition-colors cursor-pointer"
|
||||
>
|
||||
{isCopied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{url}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
// Dev tab 内容
|
||||
const DevTabContent = ({ linkItems, workspaceLink, stopWorkspace }: {
|
||||
linkItems: LinkItem[]
|
||||
workspaceLink: Partial<WorkspaceOpen>
|
||||
stopWorkspace: () => void
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => stopWorkspace()}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg bg-red-500 hover:bg-red-600 text-white font-medium transition-colors"
|
||||
>
|
||||
<Square className="w-4 h-4" />
|
||||
停止工作区
|
||||
</button>
|
||||
<div className="grid grid-cols-2 gap-3 mt-2">
|
||||
{linkItems.map((item) => (
|
||||
<LinkItem
|
||||
key={item.key}
|
||||
label={item.label}
|
||||
icon={item.icon}
|
||||
url={item.getUrl(workspaceLink)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Link tab 内容(暂留空)
|
||||
const LinkTabContent = () => {
|
||||
const store = useRepoStore(useShallow((state) => ({
|
||||
selectWorkspace: state.selectWorkspace,
|
||||
workspaceSecretLink: state.workspaceSecretLink,
|
||||
})))
|
||||
const links = store.workspaceSecretLink.map(item => ({
|
||||
label: item.title,
|
||||
url: item.value
|
||||
}))
|
||||
if (links.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-neutral-400">
|
||||
暂无链接, 或工作区未启动
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-neutral-400">
|
||||
<div className="grid grid-cols-1 gap-3 w-full max-w-sm">
|
||||
{links.map(link => (
|
||||
<LinkItem key={link.label} label={link.label} icon={<Link className="w-5 h-5" />} url={link.url} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Work tab 内容(暂留,需要根据 business_id 做事情)
|
||||
const WorkTabContent = () => {
|
||||
const store = useRepoStore(useShallow((state) => ({
|
||||
selectWorkspace: state.selectWorkspace,
|
||||
workspaceLink: state.workspaceLink,
|
||||
})))
|
||||
const businessId = store.selectWorkspace?.business_id;
|
||||
|
||||
const appList = [
|
||||
{
|
||||
title: 'Kevisual Assistant Client', key: 'Assistant Client', port: 51515, end: '/root/cli-center/'
|
||||
},
|
||||
{
|
||||
title: 'OpenCode', key: 'OpenCode', port: 100, end: ''
|
||||
},
|
||||
{
|
||||
title: 'OpenClaw', key: 'OpenClaw', port: 80, end: '/openclaw'
|
||||
},
|
||||
{
|
||||
key: 'vscode' as LinkItemKey,
|
||||
title: 'VS Code',
|
||||
icon: <Code2 className="w-5 h-5" />,
|
||||
},
|
||||
{
|
||||
title: 'OpenWebUI', key: 'OpenWebUI', port: 200, end: ''
|
||||
},
|
||||
]
|
||||
const links = appList.map(app => {
|
||||
if (app.icon) {
|
||||
return {
|
||||
label: app.title,
|
||||
icon: app.icon,
|
||||
url: store?.workspaceLink?.[app.key as LinkItemKey] as string | undefined
|
||||
}
|
||||
}
|
||||
const url = `https://${businessId}-${app.port}.cnb.run${app.end}`
|
||||
return {
|
||||
label: app.title,
|
||||
icon: <Terminal className="w-5 h-5" />,
|
||||
url
|
||||
}
|
||||
})
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-2 text-neutral-400">
|
||||
<div className="grid grid-cols-1 gap-3 w-full max-w-sm">
|
||||
{links.map(link => (
|
||||
<LinkItem key={link.label} label={link.label} icon={link.icon} url={link.url} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function WorkspaceDetailDialog() {
|
||||
const { showWorkspaceDialog, setShowWorkspaceDialog, workspaceLink, stopWorkspace, workspaceTab, setWorkspaceTab, getWorkspaceSecretLink, selectWorkspace, workspaceSecretLink } = useRepoStore(useShallow((state) => ({
|
||||
showWorkspaceDialog: state.showWorkspaceDialog,
|
||||
setShowWorkspaceDialog: state.setShowWorkspaceDialog,
|
||||
workspaceLink: state.workspaceLink,
|
||||
stopWorkspace: state.stopWorkspace,
|
||||
workspaceTab: state.workspaceTab,
|
||||
setWorkspaceTab: state.setWorkspaceTab,
|
||||
selectWorkspace: state.selectWorkspace,
|
||||
getWorkspaceSecretLink: state.getWorkspaceSecretLink,
|
||||
workspaceSecretLink: state.workspaceSecretLink
|
||||
})))
|
||||
const linkItems: LinkItem[] = [
|
||||
{
|
||||
key: 'jumpUrl' as LinkItemKey,
|
||||
label: 'Jump',
|
||||
icon: <ExternalLink className="w-5 h-5" />,
|
||||
order: 1,
|
||||
getUrl: (data) => data.jumpUrl
|
||||
},
|
||||
{
|
||||
key: 'webide' as LinkItemKey,
|
||||
label: 'Web IDE',
|
||||
icon: <Code2 className="w-5 h-5" />,
|
||||
order: 2,
|
||||
getUrl: (data) => data.webide
|
||||
},
|
||||
{
|
||||
key: 'vscode' as LinkItemKey,
|
||||
label: 'VS Code',
|
||||
icon: <Code2 className="w-5 h-5" />,
|
||||
order: 3,
|
||||
getUrl: (data) => data.vscode
|
||||
},
|
||||
{
|
||||
key: 'cursor' as LinkItemKey,
|
||||
label: 'Cursor',
|
||||
icon: <MousePointer2 className="w-5 h-5" />,
|
||||
order: 4,
|
||||
getUrl: (data) => data.cursor
|
||||
},
|
||||
{
|
||||
key: 'trae-cn' as LinkItemKey,
|
||||
label: 'Trae',
|
||||
icon: <Rocket className="w-5 h-5" />,
|
||||
order: 5,
|
||||
getUrl: (data) => data['trae-cn']
|
||||
},
|
||||
{
|
||||
key: 'windsurf' as LinkItemKey,
|
||||
label: 'Windsurf',
|
||||
icon: <Wind className="w-5 h-5" />,
|
||||
order: 6,
|
||||
getUrl: (data) => data.windsurf
|
||||
},
|
||||
{
|
||||
key: 'antigravity' as LinkItemKey,
|
||||
label: 'Antigravity',
|
||||
icon: <Plane className="w-5 h-5" />,
|
||||
order: 7,
|
||||
getUrl: (data) => data.antigravity
|
||||
},
|
||||
{
|
||||
key: 'ssh' as LinkItemKey,
|
||||
label: 'SSH',
|
||||
icon: <Lock className="w-5 h-5" />,
|
||||
order: 9,
|
||||
getUrl: (data) => data.ssh
|
||||
},
|
||||
{
|
||||
key: 'remoteSsh' as LinkItemKey,
|
||||
label: 'Remote SSH',
|
||||
icon: <Radio className="w-5 h-5" />,
|
||||
order: 10,
|
||||
getUrl: (data) => data.remoteSsh
|
||||
},
|
||||
{
|
||||
key: 'codebuddycn' as LinkItemKey,
|
||||
label: 'CodeBuddy',
|
||||
icon: <Zap className="w-5 h-5" />,
|
||||
order: 11,
|
||||
getUrl: (data) => data.codebuddycn
|
||||
},
|
||||
].sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
useEffect(() => {
|
||||
if (selectWorkspace) {
|
||||
getWorkspaceSecretLink(selectWorkspace)
|
||||
}
|
||||
}, [selectWorkspace])
|
||||
return (
|
||||
<Dialog open={showWorkspaceDialog} onOpenChange={setShowWorkspaceDialog}>
|
||||
<DialogContent className="max-w-md! bg-white">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-neutral-900">工作区</DialogTitle>
|
||||
</DialogHeader>
|
||||
{/* Tab 导航 */}
|
||||
<div className="flex border-b border-neutral-200">
|
||||
<button
|
||||
onClick={() => setWorkspaceTab('dev')}
|
||||
className={`cursor-pointer flex-1 px-4 py-3 text-sm font-medium transition-colors relative ${workspaceTab === 'dev'
|
||||
? 'text-neutral-900'
|
||||
: 'text-neutral-500 hover:text-neutral-700'
|
||||
}`}
|
||||
>
|
||||
Dev
|
||||
{workspaceTab === 'dev' && (
|
||||
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-neutral-900" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setWorkspaceTab('work')}
|
||||
className={`cursor-pointer flex-1 px-4 py-3 text-sm font-medium transition-colors relative ${workspaceTab === 'work'
|
||||
? 'text-neutral-900'
|
||||
: 'text-neutral-500 hover:text-neutral-700'
|
||||
}`}
|
||||
>
|
||||
Work
|
||||
{workspaceTab === 'work' && (
|
||||
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-neutral-900" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setWorkspaceTab('link')}
|
||||
className={clsx(`cursor-pointer flex-1 px-4 py-3 text-sm font-medium transition-colors relative ${workspaceTab === 'link'
|
||||
? 'text-neutral-900'
|
||||
: 'text-neutral-500 hover:text-neutral-700'
|
||||
}`)}
|
||||
>
|
||||
<Link className="w-4 h-4 inline-block mr-1" />
|
||||
Link
|
||||
{workspaceTab === 'link' && (
|
||||
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-neutral-900" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{/* Tab 内容 */}
|
||||
<div className="py-2">
|
||||
{workspaceTab === 'dev' && (
|
||||
<DevTabContent linkItems={linkItems} workspaceLink={workspaceLink} stopWorkspace={stopWorkspace} />
|
||||
)}
|
||||
{workspaceTab === 'link' && (
|
||||
<LinkTabContent />
|
||||
)}
|
||||
{workspaceTab === 'work' && (
|
||||
<WorkTabContent />
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
210
src/pages/repos/page.tsx
Normal file
210
src/pages/repos/page.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import { use, useEffect, useMemo, useState } from 'react'
|
||||
import { useRepoStore } from './store/index'
|
||||
import { useShallow } from 'zustand/shallow'
|
||||
import { RepoCard } from './components/RepoCard'
|
||||
import { EditRepoDialog } from './modules/EditRepoDialog'
|
||||
import { CreateRepoDialog } from './modules/CreateRepoDialog'
|
||||
import { WorkspaceDetailDialog } from './modules/WorkspaceDetailDialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { ExternalLinkIcon, Plus, RefreshCw, Search, Settings } from 'lucide-react'
|
||||
import Fuse from 'fuse.js'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { useLayoutStore } from '../auth/store'
|
||||
import { useConfigStore } from '../config/store'
|
||||
import { SidebarLayout } from '../sidebar/components'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
|
||||
export const App = () => {
|
||||
const { list, refresh, loading, workspaceList, setShowCreateDialog } = useRepoStore(useShallow((state) => ({
|
||||
list: state.list,
|
||||
refresh: state.refresh,
|
||||
loading: state.loading,
|
||||
workspaceList: state.workspaceList,
|
||||
setShowCreateDialog: state.setShowCreateDialog,
|
||||
})))
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [filterDev, setFilterDev] = useState(() => {
|
||||
const saved = localStorage.getItem('repos-filter-dev')
|
||||
return saved === 'true'
|
||||
})
|
||||
const navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
refresh({ showTips: false })
|
||||
}, [])
|
||||
|
||||
const appList = useMemo(() => {
|
||||
const sortedList = [...list].sort((a, b) => {
|
||||
const aActive = workspaceList.some(ws => ws.slug === a.path)
|
||||
const bActive = workspaceList.some(ws => ws.slug === b.path)
|
||||
|
||||
if (aActive && !bActive) return -1
|
||||
if (!aActive && bActive) return 1
|
||||
return 0
|
||||
})
|
||||
|
||||
let filteredList = sortedList
|
||||
if (filterDev) {
|
||||
filteredList = sortedList.filter(repo => {
|
||||
const topics = repo.topics ? repo.topics.split(',').map(t => t.trim().toLowerCase()) : []
|
||||
return topics.some(topic => topic.includes('dev'))
|
||||
})
|
||||
}
|
||||
|
||||
if (!searchQuery.trim()) {
|
||||
return filteredList
|
||||
}
|
||||
|
||||
const fuse = new Fuse(filteredList, {
|
||||
keys: ['name', 'path', 'description'],
|
||||
threshold: 0.3,
|
||||
includeScore: true
|
||||
})
|
||||
|
||||
const results = fuse.search(searchQuery)
|
||||
return results.map(result => result.item)
|
||||
}, [list, workspaceList, searchQuery, filterDev])
|
||||
|
||||
const isCNB = location.hostname.includes('cnb.run')
|
||||
return (
|
||||
<SidebarLayout>
|
||||
<div className="min-h-screen bg-neutral-50 flex flex-col">
|
||||
<div className="container mx-auto p-4 md:p-6 max-w-7xl flex-1">
|
||||
<div className="mb-6 md:mb-8 flex flex-col gap-4">
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between gap-3">
|
||||
<div className=''>
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl md:text-4xl font-bold text-neutral-900 flex gap-2 items-center">
|
||||
<span className="hidden md:inline">仓库列表</span>
|
||||
<span className="md:hidden">仓库</span>
|
||||
<Settings className="inline-block h-5 w-5 text-neutral-400 hover:text-neutral-600 cursor-pointer" onClick={() => navigate({ to: '/config' })} />
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-neutral-600 text-sm md:text-base">
|
||||
{filterDev ? `显示 ${appList.length} 个 dev 仓库` : `共 ${list.length} 个仓库`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 md:ml-auto">
|
||||
<Button
|
||||
onClick={() => refresh()}
|
||||
variant="outline"
|
||||
className="gap-2 flex-1 sm:flex-none"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">刷新</span>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setShowCreateDialog(true)}
|
||||
className="gap-2 flex-1 sm:flex-none"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">新建仓库</span>
|
||||
<span className="sm:hidden">新建</span>
|
||||
</Button>
|
||||
|
||||
{isCNB && <Button
|
||||
onClick={() => {
|
||||
window.open('/root/cli-center', '_blank')
|
||||
}}
|
||||
className="gap-2 hidden md:flex"
|
||||
>
|
||||
<ExternalLinkIcon className="h-4 w-4" />
|
||||
CLI
|
||||
</Button>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 md:mb-6">
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-neutral-400" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="搜索仓库..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="filter-dev"
|
||||
checked={filterDev}
|
||||
onCheckedChange={(checked) => {
|
||||
const value = checked === true
|
||||
setFilterDev(value)
|
||||
localStorage.setItem('repos-filter-dev', String(value))
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
htmlFor="filter-dev"
|
||||
className="text-sm text-neutral-600 cursor-pointer select-none"
|
||||
>
|
||||
过滤 dev
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6">
|
||||
{appList.map((repo) => (
|
||||
<RepoCard
|
||||
key={repo.id}
|
||||
repo={repo}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{appList.length === 0 && !loading && (
|
||||
<div className="text-center py-20">
|
||||
<div className="text-neutral-400 text-lg">
|
||||
{searchQuery ? '未找到匹配的仓库' : '暂无仓库数据'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<footer className="border-t border-neutral-200 bg-white py-4 md:py-6 mt-auto">
|
||||
<div className="container mx-auto px-4 md:px-6 max-w-7xl">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-2 md:gap-4 text-sm text-neutral-500">
|
||||
<div>© 2026 仓库管理系统</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<a href="#" className="hover:text-neutral-900 transition-colors">关于</a>
|
||||
<a href="#" className="hover:text-neutral-900 transition-colors">帮助</a>
|
||||
<a href="#" className="hover:text-neutral-900 transition-colors">联系我们</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<CommonRepoDialog />
|
||||
</div>
|
||||
</SidebarLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export const CommonRepoDialog = () => {
|
||||
const { editRepo, showEditDialog, setShowEditDialog, showCreateDialog, setShowCreateDialog, } = useRepoStore(useShallow((state) => ({
|
||||
editRepo: state.editRepo,
|
||||
showEditDialog: state.showEditDialog,
|
||||
setShowEditDialog: state.setShowEditDialog,
|
||||
showCreateDialog: state.showCreateDialog,
|
||||
setShowCreateDialog: state.setShowCreateDialog,
|
||||
})))
|
||||
return (
|
||||
<>
|
||||
<EditRepoDialog
|
||||
open={showEditDialog}
|
||||
onOpenChange={setShowEditDialog}
|
||||
repo={editRepo}
|
||||
/>
|
||||
<CreateRepoDialog
|
||||
open={showCreateDialog}
|
||||
onOpenChange={setShowCreateDialog}
|
||||
/>
|
||||
<WorkspaceDetailDialog />
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default App;
|
||||
73
src/pages/repos/repo/page.tsx
Normal file
73
src/pages/repos/repo/page.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { useSearch } from "@tanstack/react-router";
|
||||
import { useRepoStore } from "../store";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useShallow } from "zustand/shallow";
|
||||
import BuildConfig from "../components/BuildConfig";
|
||||
import { CommonRepoDialog } from "../page";
|
||||
import { RepoCard } from "../components/RepoCard";
|
||||
import { SidebarLayout } from "@/pages/sidebar/components";
|
||||
|
||||
export const App = () => {
|
||||
const params = useSearch({ strict: false }) as { repo?: string, tab?: string };
|
||||
const repoStore = useRepoStore(useShallow((state) => ({
|
||||
getItem: state.getItem,
|
||||
editRepo: state.editRepo,
|
||||
refresh: state.refresh,
|
||||
loading: state.loading,
|
||||
buildConfig: state.buildConfig,
|
||||
})));
|
||||
const [activeTab, setActiveTab] = useState(params.tab || "build");
|
||||
const tabs = [
|
||||
{ key: "build", label: "构建配置" },
|
||||
{ key: "info", label: "基本信息" },
|
||||
]
|
||||
useEffect(() => {
|
||||
if (params.repo) {
|
||||
if(repoStore.buildConfig?.repo !== params.repo) {
|
||||
repoStore.getItem(params.repo);
|
||||
}
|
||||
console.log('refreshing repo',repoStore.buildConfig, params.repo)
|
||||
// repoStore.getItem(params.repo);
|
||||
repoStore.refresh({ search: params.repo, showTips: false });
|
||||
} else {
|
||||
console.log('no repo param')
|
||||
}
|
||||
}, [params])
|
||||
if (!repoStore.editRepo) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
return (
|
||||
<SidebarLayout>
|
||||
<div className="p-2 md:p-4 flex-col flex gap-2 md:gap-4 h-full">
|
||||
<div className="px-2 md:px-4 h-full scrollbar flex-col flex gap-3 md:gap-4 overflow-hidden">
|
||||
<div className="flex border-b overflow-x-auto h-12 shrink-0">
|
||||
{tabs.map(tab => (
|
||||
<div
|
||||
key={tab.key}
|
||||
className={`px-3 md:px-4 py-2 cursor-pointer whitespace-nowrap text-sm md:text-base ${activeTab === tab.key ? 'border-b-2 border-gray-500 font-medium' : ''}`}
|
||||
onClick={() => {
|
||||
setActiveTab(tab.key)
|
||||
history.replaceState(null, '', `?repo=${params.repo}&tab=${tab.key}`)
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{activeTab === 'build' && <BuildConfig />}
|
||||
{activeTab === 'info' && (
|
||||
<div className="flex flex-col gap-3 md:gap-4 h-full">
|
||||
<RepoCard repo={repoStore.editRepo} showReturn />
|
||||
<div className="p-3 md:p-4 border rounded bg-white h-full overflow-auto scrollbar">
|
||||
<pre className="whitespace-pre-wrap break-all text-xs md:text-sm">{JSON.stringify(repoStore.editRepo, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<CommonRepoDialog />
|
||||
</div>
|
||||
</SidebarLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default App;
|
||||
100
src/pages/repos/store/build.ts
Normal file
100
src/pages/repos/store/build.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
export const myOrgs = ['kevisual', 'kevision', 'skillpod', 'zxj.im', 'abearxiong']
|
||||
import dayjs from 'dayjs'
|
||||
export const createBuildConfig = (params: { repo: string }) => {
|
||||
const toRepo = params.repo!;
|
||||
return `
|
||||
# .cnb.yml
|
||||
include:
|
||||
- https://cnb.cool/kevisual/cnb/-/blob/main/.cnb/template.yml
|
||||
|
||||
.common_env: &common_env
|
||||
env:
|
||||
TO_REPO: ${toRepo}
|
||||
TO_URL: git.xiongxiao.me
|
||||
imports:
|
||||
- https://cnb.cool/kevisual/env/-/blob/main/.env.development
|
||||
|
||||
.common_sync_to_gitea: &common_sync_to_gitea
|
||||
- <<: *common_env
|
||||
services: !reference [.common_sync_to_gitea_template, services]
|
||||
stages: !reference [.common_sync_to_gitea_template, stages]
|
||||
|
||||
.common_sync_from_gitea: &common_sync_from_gitea
|
||||
- <<: *common_env
|
||||
services: !reference [.common_sync_from_gitea_template, services]
|
||||
stages: !reference [.common_sync_from_gitea_template, stages]
|
||||
|
||||
main:
|
||||
api_trigger_sync_to_gitea:
|
||||
- <<: *common_sync_to_gitea
|
||||
api_trigger_sync_from_gitea:
|
||||
- <<: *common_sync_from_gitea
|
||||
`
|
||||
};
|
||||
|
||||
export const createCommitBlankConfig = (params: { repo?: string, event: 'api_trigger_event' }) => {
|
||||
const now = dayjs().format('YYYY-MM-DD HH:mm')
|
||||
const event = params?.event || 'api_trigger_event'
|
||||
return `main:
|
||||
${event}:
|
||||
-
|
||||
services:
|
||||
- docker
|
||||
stages:
|
||||
- name: 显示 git remote
|
||||
script: git remote -v
|
||||
- name: commit_blank
|
||||
script: |
|
||||
echo "这是一个空白提交 时间: ${now}"
|
||||
git commit --allow-empty -m "up: ${now}"
|
||||
git push
|
||||
`
|
||||
}
|
||||
|
||||
export const createDevConfig = (params: { repo?: string, event?: string, branch?: string }) => {
|
||||
const event = params?.event || 'api_trigger_event';
|
||||
const branch = params?.branch || '$';
|
||||
return `##### 配置开始,保留注释 #####
|
||||
.common_env: &common_env
|
||||
env:
|
||||
USERNAME: root
|
||||
imports:
|
||||
- https://cnb.cool/\${CNB_GROUP_SLUG}/env/-/blob/main/.env
|
||||
# - https://cnb.cool/\${CNB_GROUP_SLUG}/env/-/blob/main/ssh.yml
|
||||
# - https://cnb.cool/\${CNB_GROUP_SLUG}/env/-/blob/main/ssh-config.yml
|
||||
|
||||
##### 配置结束 #####
|
||||
|
||||
${branch}:
|
||||
${event}:
|
||||
- docker:
|
||||
image: docker.cnb.cool/kevisual/dev-env:latest
|
||||
services:
|
||||
- vscode
|
||||
- docker
|
||||
imports: !reference [.common_env, imports]
|
||||
env: !reference [.common_env, env]
|
||||
runner:
|
||||
cpus: $RUN_CPU
|
||||
#tags: cnb:arch:amd64:gpu
|
||||
stages:
|
||||
- name: 安装dev-cnb的仓库代码模块
|
||||
script: |
|
||||
cd /workspace && find . -mindepth 1 -delete
|
||||
git init
|
||||
git remote add origin https://cnb.cool/kevisual/dev-cnb
|
||||
git fetch origin main
|
||||
git reset --hard origin/main
|
||||
- name: 启动nginx
|
||||
script: nginx
|
||||
- name: 启动搜索服务
|
||||
script: zsh -i -c 'bun src/cli.ts init start-meilisearch'
|
||||
- name: 初始化开发机
|
||||
script: zsh -i -c 'bun run start'
|
||||
- name: 启动当前工作区
|
||||
script: zsh -i -c 'cloud cnb keep-alive-current-workspace'
|
||||
# endStages:
|
||||
# - name: 结束阶段
|
||||
# script: zsh -i -c 'bun run end'
|
||||
`
|
||||
}
|
||||
572
src/pages/repos/store/index.ts
Normal file
572
src/pages/repos/store/index.ts
Normal file
@@ -0,0 +1,572 @@
|
||||
import { create } from 'zustand';
|
||||
import { query } from '@/modules/query';
|
||||
import { toast } from 'sonner';
|
||||
import { queryApi as cnbApi } from '@/modules/cnb-api'
|
||||
import { WorkspaceInfo } from '@kevisual/cnb'
|
||||
import { createCommitBlankConfig, createDevConfig } from './build';
|
||||
import { useLayoutStore } from '@/pages/auth/store';
|
||||
import { Query } from '@kevisual/query';
|
||||
interface DisplayModule {
|
||||
activity: boolean;
|
||||
contributors: boolean;
|
||||
release: boolean;
|
||||
}
|
||||
|
||||
interface Languages {
|
||||
language: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface Data {
|
||||
id: string;
|
||||
name: string;
|
||||
freeze: boolean;
|
||||
status: number;
|
||||
// Public, Private
|
||||
visibility_level: string;
|
||||
flags: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
description: string;
|
||||
site: string;
|
||||
topics: string;
|
||||
license: string;
|
||||
display_module: DisplayModule;
|
||||
star_count: number;
|
||||
fork_count: number;
|
||||
mark_count: number;
|
||||
last_updated_at?: string | null;
|
||||
web_url: string;
|
||||
path: string;
|
||||
tags: any;
|
||||
open_issue_count: number;
|
||||
open_pull_request_count: number;
|
||||
languages: Languages;
|
||||
second_languages: Languages;
|
||||
last_update_username: string;
|
||||
last_update_nickname: string;
|
||||
access: string;
|
||||
stared: boolean;
|
||||
star_time: string;
|
||||
pinned: boolean;
|
||||
pinned_time: string;
|
||||
}
|
||||
|
||||
type WorkspaceTabType = 'dev' | 'work' | 'link'
|
||||
|
||||
type BuildConfig = {
|
||||
repo: string;
|
||||
branch: string;
|
||||
event: string;
|
||||
config: string;
|
||||
}
|
||||
type State = {
|
||||
formData: Record<string, any>;
|
||||
setFormData: (data: Record<string, any>) => void;
|
||||
showEdit: boolean;
|
||||
setShowEdit: (showEdit: boolean) => void;
|
||||
loading: boolean;
|
||||
setLoading: (loading: boolean) => void;
|
||||
workspaceTab: WorkspaceTabType;
|
||||
setWorkspaceTab: (tab: WorkspaceTabType) => void;
|
||||
list: Data[];
|
||||
editRepo: Data | null;
|
||||
setEditRepo: (repo: Data | null) => void;
|
||||
showEditDialog: boolean;
|
||||
setShowEditDialog: (show: boolean) => void;
|
||||
showCreateDialog: boolean;
|
||||
setShowCreateDialog: (show: boolean) => void;
|
||||
getList: (params?: { search?: string }, silent?: boolean) => Promise<any>;
|
||||
updateRepoInfo: (data: Partial<Data & { topics: string[] }>) => Promise<any>;
|
||||
createRepo: (data: { visibility: any, path: string, description: string, license: string }) => Promise<any>;
|
||||
deleteItem: (repo: string) => Promise<any>;
|
||||
workspaceList: WorkspaceInfo[];
|
||||
getWorkspaceList: () => Promise<any>;
|
||||
refresh: (opts?: { message?: string, showTips?: boolean, search?: string }) => Promise<any>;
|
||||
startWorkspace: (data: Partial<Data>, params?: { open?: boolean, branch?: string }) => Promise<any>;
|
||||
stopWorkspace: (workspace?: WorkspaceInfo) => Promise<any>;
|
||||
getWorkspaceDetail: (data: WorkspaceInfo) => Promise<any>;
|
||||
workspaceLink: Partial<WorkspaceOpen>;
|
||||
selectWorkspace?: WorkspaceInfo,
|
||||
showWorkspaceDialog: boolean;
|
||||
setShowWorkspaceDialog: (show: boolean) => void;
|
||||
syncDialogOpen: boolean;
|
||||
setSyncDialogOpen: (open: boolean) => void;
|
||||
selectedSyncRepo: Data | null;
|
||||
setSelectedSyncRepo: (repo: Data | null) => void;
|
||||
buildUpdate: (data: Partial<Data>, params?: any) => Promise<any>;
|
||||
getItem: (repo: string) => Promise<any>;
|
||||
buildConfig: BuildConfig | null;
|
||||
setBuildConfig: (config: BuildConfig | null, save?: boolean) => Promise<any>;
|
||||
deleteBuildConfig: (params: { repo: Data, user?: any }) => Promise<any>;
|
||||
initBuildConfig: (params: { repo: Data, user?: any }) => Promise<any>;
|
||||
buildWorkspace: () => Promise<any>;
|
||||
workspaceSecretLink: { title: string, key: string, value?: string }[];
|
||||
getWorkspaceSecretLink: (workspace: WorkspaceInfo) => Promise<any>;
|
||||
}
|
||||
|
||||
export const useRepoStore = create<State>((set, get) => {
|
||||
return {
|
||||
formData: {},
|
||||
setFormData: (data) => set({ formData: data }),
|
||||
showEdit: false,
|
||||
setShowEdit: (showEdit) => set({ showEdit }),
|
||||
loading: false,
|
||||
setLoading: (loading) => set({ loading }),
|
||||
list: [],
|
||||
editRepo: null,
|
||||
setEditRepo: (repo) => set({ editRepo: repo }),
|
||||
showEditDialog: false,
|
||||
setShowEditDialog: (show) => set({ showEditDialog: show }),
|
||||
showCreateDialog: false,
|
||||
setShowCreateDialog: (show) => set({ showCreateDialog: show }),
|
||||
showWorkspaceDialog: false,
|
||||
setShowWorkspaceDialog: (show) => set({ showWorkspaceDialog: show }),
|
||||
workspaceTab: 'dev',
|
||||
setWorkspaceTab: (tab) => set({ workspaceTab: tab }),
|
||||
syncDialogOpen: false,
|
||||
setSyncDialogOpen: (open) => set({ syncDialogOpen: open }),
|
||||
selectedSyncRepo: null,
|
||||
setSelectedSyncRepo: (repo) => set({ selectedSyncRepo: repo }),
|
||||
buildConfig: null,
|
||||
setBuildConfig: async (config, save = true) => {
|
||||
const me = useLayoutStore.getState().me;
|
||||
if (config && config!.repo && save) {
|
||||
let path: string = config.repo || '';
|
||||
path = path.replace(/\//g, '__');
|
||||
const key = `buildConfig_${path}.json`;
|
||||
if (config && me) {
|
||||
const res = await query.post({
|
||||
path: 'config',
|
||||
key: 'update',
|
||||
data: {
|
||||
key: key,
|
||||
data: config,
|
||||
}
|
||||
})
|
||||
if (res.code === 200) {
|
||||
toast.success('配置已保存')
|
||||
} else {
|
||||
toast.error(res.message || '配置保存失败')
|
||||
}
|
||||
} else if (config) {
|
||||
localStorage.setItem(key, JSON.stringify(config));
|
||||
}
|
||||
}
|
||||
|
||||
set({ buildConfig: config })
|
||||
},
|
||||
deleteBuildConfig: async (params: { repo: Data, user?: any }) => {
|
||||
const repo = params.repo;
|
||||
let path: string = repo.path || '';
|
||||
path = path.replace(/\//g, '__');
|
||||
const key = `buildConfig_${path}.json`;
|
||||
if (params?.user) {
|
||||
const res = await query.post({
|
||||
path: 'config',
|
||||
key: 'delete',
|
||||
data: {
|
||||
key: key,
|
||||
}
|
||||
})
|
||||
if (res.code === 200) {
|
||||
toast.success('配置已删除')
|
||||
} else {
|
||||
toast.error(res.message || '配置删除失败')
|
||||
}
|
||||
} else {
|
||||
localStorage.removeItem(key);
|
||||
toast.success('配置已删除')
|
||||
}
|
||||
},
|
||||
initBuildConfig: async (params) => {
|
||||
const repo = params.repo;
|
||||
if (!repo) {
|
||||
toast.error('仓库数据异常');
|
||||
return;
|
||||
}
|
||||
set({ loading: true })
|
||||
try {
|
||||
console.log('初始化构建配置', params)
|
||||
let path: string = repo.path || '';
|
||||
path = path.replace(/\//g, '__');
|
||||
const key = `buildConfig_${path}.json`;
|
||||
if (params?.user) {
|
||||
const res = await query.post({
|
||||
path: 'config',
|
||||
key: 'get',
|
||||
data: {
|
||||
key: key,
|
||||
}
|
||||
})
|
||||
if (res.code === 200 && res.data?.data) {
|
||||
set({ buildConfig: res.data.data })
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const localConfig = localStorage.getItem(key);
|
||||
if (localConfig) {
|
||||
try {
|
||||
const config = JSON.parse(localConfig);
|
||||
set({ buildConfig: config })
|
||||
return;
|
||||
} catch (e) {
|
||||
console.error('本地配置解析失败', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
const config: BuildConfig = {
|
||||
repo: repo.path,
|
||||
branch: 'main',
|
||||
event: 'api_trigger_event',
|
||||
config: createDevConfig({ repo: repo.path, event: 'api_trigger_event', branch: 'main' }),
|
||||
}
|
||||
set({ buildConfig: config })
|
||||
} catch (e) {
|
||||
toast.error('配置加载失败');
|
||||
console.error('配置加载失败', e);
|
||||
}
|
||||
finally {
|
||||
set({ loading: false })
|
||||
}
|
||||
|
||||
},
|
||||
buildWorkspace: async () => {
|
||||
const config = get().buildConfig;
|
||||
if (!config) {
|
||||
toast.error('请先保存构建配置');
|
||||
return;
|
||||
}
|
||||
const res = await cnbApi.cnb['cloud-build']({
|
||||
repo: config.repo,
|
||||
branch: config.branch,
|
||||
env: {} as any,
|
||||
event: config.event,
|
||||
config: config.config,
|
||||
})
|
||||
if (res.code === 200) {
|
||||
toast.success('构建已触发')
|
||||
} else {
|
||||
toast.error(res.message || '构建触发失败')
|
||||
}
|
||||
},
|
||||
workspaceSecretLink: [],
|
||||
getWorkspaceSecretLink: async (workspace) => {
|
||||
console.log('获取工作区链接', workspace)
|
||||
const business_id = workspace?.business_id;
|
||||
const baseURL = `https://${business_id}-51515.cnb.run/client/router`;
|
||||
console.log('工作区链接', baseURL)
|
||||
const url = new URL(baseURL);
|
||||
const token = localStorage.getItem('token');
|
||||
// const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtpZC1rZXktMSJ9.eyJzdWIiOiJ1c2VyOjBlNzAwZGM4LTkwZGQtNDFiNy05MWRkLTMzNmVhNTFkZTNkMiIsIm5hbWUiOiJyb290IiwiZXhwIjoxNzczMTI4NTMwLCJpc3MiOiJodHRwczovL2NvbnZleC5rZXZpc3VhbC5jbiIsImlhdCI6MTc3MzEyMTMzMCwiYXVkIjoiY29udmV4LWFwcCJ9.g4kANiPc352QFBfa0yb4gl98mLHTruL_3HvIaKYwN1Qy3-P8QV6X_WhqgMOskQphNGsBFC-LRmZq2808GnqwpjDTE0ekXbsO4L9C-D6F3mBMwowqpvmURCRVg6Ys6LSkzw4sM75VbHpfFX3ZQVtZymvAWhxxxvjhdKGPdrdw5bNymTbCw-Y9NrYW6u2mExLrvrfXl3vJqaCz7obj_mR-G_2PB3g5KPQYhWCl8--TkYOS9fiNIYlcacnO36bZXhHheHFZEr_gb8UG5ECg0ND8hsH8TijiYBAY6T93nhGrZG7E0oQY3xXsVm-mkvXP2tLCXwKH7SFmH4M0tdZLRqLqKw'
|
||||
url.searchParams.set('path', 'cnb_board');
|
||||
url.searchParams.set('key', 'live');
|
||||
const res = await fetch(url.toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
}).then(res => res.json());
|
||||
const labelData: { title: string, key: string, value?: string }[] = [
|
||||
{
|
||||
title: 'Opencode Secret',
|
||||
key: 'opencodeUrlSecret',
|
||||
},
|
||||
{
|
||||
title: 'Openclaw Secret',
|
||||
key: 'openclawUrlSecret',
|
||||
},
|
||||
{
|
||||
title: 'StartTime',
|
||||
key: 'buildStartTime',
|
||||
}
|
||||
];
|
||||
if (res.code === 200) {
|
||||
const list = res.data?.list || [];
|
||||
const workspaceSecretLink: { title: string, key: string, value?: string }[] = [];
|
||||
labelData.forEach(item => {
|
||||
const find = list.find((l: any) => l.key === item.key);
|
||||
if (find) {
|
||||
workspaceSecretLink.push({ ...item, value: find.value });
|
||||
}
|
||||
})
|
||||
set({ workspaceSecretLink })
|
||||
}
|
||||
},
|
||||
getItem: async (repo: string) => {
|
||||
const { setLoading } = get();
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await cnbApi.cnb['get-repo']({ name: repo })
|
||||
if (res.code === 200) {
|
||||
const data = res.data!;
|
||||
set({ editRepo: data })
|
||||
} else {
|
||||
toast.error(res.message || '请求失败');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
getList: async (params?: { search?: string }, silent = false) => {
|
||||
const { setLoading } = get();
|
||||
if (!silent) {
|
||||
setLoading(true);
|
||||
}
|
||||
try {
|
||||
let opts = {}
|
||||
if (params?.search) {
|
||||
opts = {
|
||||
search: params.search
|
||||
}
|
||||
}
|
||||
const res = await cnbApi.cnb['list-repos'](opts)
|
||||
if (res.code === 200) {
|
||||
const list = res.data?.list || []
|
||||
set({ list });
|
||||
} else {
|
||||
toast.error(res.message || '请求失败');
|
||||
}
|
||||
return res;
|
||||
} finally {
|
||||
if (!silent) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
updateRepoInfo: async (data) => {
|
||||
const repo = data.path!;
|
||||
let topics = data.topics as string[];
|
||||
if (Array.isArray(topics)) {
|
||||
topics = topics.map(t => t.trim()).filter(Boolean);
|
||||
}
|
||||
if (topics?.length === 0) {
|
||||
topics.push('cnb-center')
|
||||
}
|
||||
const updateData = {
|
||||
description: data.description!,
|
||||
license: data?.license as any,
|
||||
site: data.site,
|
||||
topics: topics
|
||||
}
|
||||
const res = await cnbApi.cnb['update-repo-info']({ name: repo, ...updateData })
|
||||
if (res.code === 200) {
|
||||
toast.success('更新成功');
|
||||
} else {
|
||||
toast.error(res.message || '更新失败');
|
||||
}
|
||||
},
|
||||
refresh: async (opts?: { message?: string, showTips?: boolean, search?: string }) => {
|
||||
const getList = get().getList({ search: opts?.search }, true);
|
||||
|
||||
|
||||
const getWorkspaceList = get().getWorkspaceList();
|
||||
await Promise.all([getList, getWorkspaceList]);
|
||||
if (opts?.showTips !== false) {
|
||||
toast.success(opts?.message || '刷新成功');
|
||||
}
|
||||
},
|
||||
createRepo: async (data) => {
|
||||
try {
|
||||
const createData = {
|
||||
name: data.path || '',
|
||||
visibility: data.visibility || 'public' as const,
|
||||
description: data.description || '',
|
||||
license: data?.license as any,
|
||||
};
|
||||
const res = await cnbApi.cnb['create-repo'](createData);
|
||||
console.log('res', res)
|
||||
// if (res.code === 200) {
|
||||
// toast.success('仓库创建成功');
|
||||
// } else {
|
||||
// toast.error(res.message || '创建失败');
|
||||
// }
|
||||
return res;
|
||||
} catch (e: any) {
|
||||
// toast.error(e.message || '创建失败');
|
||||
// throw e;
|
||||
toast.success('仓库创建成功');
|
||||
}
|
||||
},
|
||||
deleteItem: async (repo: string) => {
|
||||
try {
|
||||
const res = await cnbApi.cnb['delete-repo']({ name: repo });
|
||||
if (res.code === 200) {
|
||||
toast.success('删除成功');
|
||||
// 刷新列表
|
||||
await get().getList({}, true);
|
||||
} else {
|
||||
toast.error(res.message || '删除失败');
|
||||
}
|
||||
} catch (e: any) {
|
||||
// 如果是 JSON 解析错误,说明删除成功但响应为空
|
||||
if (e.message?.includes('JSON') || e.message?.includes('json')) {
|
||||
toast.success('删除成功');
|
||||
// 刷新列表
|
||||
await get().getList({}, true);
|
||||
} else {
|
||||
toast.error('删除失败');
|
||||
console.error('删除错误:', e);
|
||||
}
|
||||
}
|
||||
},
|
||||
workspaceList: [],
|
||||
getWorkspaceList: async () => {
|
||||
// const res = await cnb.workspace.list({
|
||||
const res = await cnbApi.cnb['list-workspace']({
|
||||
status: 'running',
|
||||
pageSize: 100
|
||||
})
|
||||
if (res.code === 200) {
|
||||
const list: WorkspaceInfo[] = res.data?.list as any;
|
||||
set({ workspaceList: list || [] })
|
||||
} else {
|
||||
toast.error(res.message || '请求失败');
|
||||
}
|
||||
},
|
||||
startWorkspace: async (data, params = { open: true, branch: 'main' }) => {
|
||||
const repo = data.path;
|
||||
const checkOpen = async () => {
|
||||
const res = await cnbApi.cnb['start-workspace']({ repo: repo!, branch: params.branch || 'main' });
|
||||
if (res.code === 200) {
|
||||
if (!res?.data?.sn) {
|
||||
const url = res.data?.url! || '';
|
||||
if (url.includes('loading')) {
|
||||
return {
|
||||
code: 400,
|
||||
data: res.data
|
||||
}
|
||||
}
|
||||
return {
|
||||
code: 200,
|
||||
data: res.data
|
||||
};
|
||||
}
|
||||
toast.success(`新创建了一个工作区,sn: ${res.data.sn}`)
|
||||
return {
|
||||
code: 300,
|
||||
data: res.data,
|
||||
message: '第一次启动'
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
code: res.code,
|
||||
message: res.message || '请求失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
const res = await checkOpen()
|
||||
if (res.code === 300) {
|
||||
if (params.open) {
|
||||
const loadingToastId = toast.loading('正在启动工作区...')
|
||||
const interval = setInterval(async () => {
|
||||
const check = await checkOpen()
|
||||
if (check.code === 200 && check.data?.url) {
|
||||
clearInterval(interval)
|
||||
toast.dismiss(loadingToastId)
|
||||
toast.success(`工作区已启动,正在打开: ${check.data.url}`)
|
||||
get().refresh({ showTips: false })
|
||||
setTimeout(() => {
|
||||
window.open(check.data?.url, '_blank')
|
||||
}, 200)
|
||||
} else if (check.code === 400) {
|
||||
//
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// 30秒后自动停止检测
|
||||
setTimeout(() => {
|
||||
clearInterval(interval)
|
||||
toast.dismiss(loadingToastId)
|
||||
toast.error('工作区启动超时')
|
||||
}, 3 * 60 * 1000)
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (res.code === 200 && res.data?.url) {
|
||||
console.log('res', res)
|
||||
toast.success(`工作区已启动,正在打开: ${res.data.url}`)
|
||||
setTimeout(() => {
|
||||
window.open(res.data?.url, '_blank')
|
||||
}, 200)
|
||||
} else {
|
||||
toast.error(res.message || '启动失败');
|
||||
}
|
||||
return res;
|
||||
},
|
||||
stopWorkspace: async (workspace?: WorkspaceInfo) => {
|
||||
const sn = workspace?.sn || get().selectWorkspace?.sn;
|
||||
if (!sn) {
|
||||
toast.error('未选择工作区');
|
||||
return;
|
||||
}
|
||||
const res = await cnbApi.cnb['stop-workspace']({ sn })
|
||||
// @ts-ignore
|
||||
if (res?.code === 200) {
|
||||
toast.success('工作区已停止');
|
||||
// 停止成功后关闭弹窗
|
||||
set({ showWorkspaceDialog: false });
|
||||
get().refresh({ showTips: false });
|
||||
} else {
|
||||
toast.error(res.message || '停止失败');
|
||||
}
|
||||
},
|
||||
selectWorkspace: undefined,
|
||||
getWorkspaceDetail: async (workspaceInfo) => {
|
||||
const res = await cnbApi.cnb['get-workspace']({ repo: workspaceInfo.slug, sn: workspaceInfo.sn }) as any;
|
||||
if (res.code === 200) {
|
||||
set({
|
||||
workspaceLink: res.data,
|
||||
showWorkspaceDialog: true,
|
||||
selectWorkspace: workspaceInfo
|
||||
})
|
||||
}
|
||||
},
|
||||
workspaceLink: {},
|
||||
buildUpdate: async (data) => {
|
||||
const res = await cnbApi.cnb['cloud-build']({
|
||||
repo: data.path!,
|
||||
branch: 'main',
|
||||
env: {} as any,
|
||||
event: 'api_trigger_event',
|
||||
config: createCommitBlankConfig({ repo: data.path!, event: 'api_trigger_event' }),
|
||||
})
|
||||
if (res.code === 200) {
|
||||
toast.success('更新成功')
|
||||
setTimeout(() => {
|
||||
get().refresh({ showTips: false })
|
||||
}, 5000)
|
||||
} else {
|
||||
toast.error(res.message || '更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
export type WorkspaceOpen = {
|
||||
url?: string
|
||||
webide?: string
|
||||
jumpUrl?: string
|
||||
remoteSsh?: string
|
||||
jetbrains?: Record<string, string>
|
||||
codebuddy?: string
|
||||
codebuddycn?: string
|
||||
vscode?: string
|
||||
cursor?: string
|
||||
'vscode-insiders'?: string
|
||||
trae?: string
|
||||
'trae-cn'?: string
|
||||
windsurf?: string
|
||||
'windsurf-next'?: string
|
||||
antigravity?: string
|
||||
ssh?: string
|
||||
}
|
||||
const openWorkspace = (workspace: WorkspaceInfo, params: { vscode?: boolean, ssh?: boolean }) => {
|
||||
const openVsCode = params?.vscode ?? true;
|
||||
if (openVsCode) {
|
||||
const pipeline_id = workspace.pipeline_id;
|
||||
const url = `vscode://vscode-remote/ssh-remote+${pipeline_id}`
|
||||
}
|
||||
}
|
||||
11
src/pages/sidebar/components/CNBBlackLogo.tsx
Normal file
11
src/pages/sidebar/components/CNBBlackLogo.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
export const Logo = (props: React.SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg {...props} width="320" height="320" viewBox="0 0 320 320" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M228.906 40.2412C229.882 37.5108 228.906 34.3903 226.759 32.44C219.342 26.004 200.799 12.3519 173.082 10.4016C141.852 8.06121 122.528 16.4475 112.769 22.6885C108.474 25.4189 108.279 31.4649 112.183 34.3903L191.625 96.2149C198.652 101.676 208.997 98.5553 211.729 90.169L228.711 40.2412H228.906Z" fill="black" />
|
||||
<path d="M32.9381 223.564C29.6199 225.71 28.2536 229.805 29.2295 233.511C32.1573 244.432 41.3312 266.861 66.9009 287.534C92.4706 308.012 122.725 310.353 135.607 309.963C139.511 309.963 142.829 307.427 144 303.722L194.945 142.627C198.653 130.925 185.576 121.173 175.426 127.999L32.9381 223.564Z" fill="black" />
|
||||
<path d="M70.2169 53.4955C67.6794 52.5203 64.9468 52.7153 62.6045 53.8855C53.2355 58.9563 29.032 74.7538 16.54 107.324C6.78054 132.288 10.0987 159.982 12.8314 173.439C13.6121 177.925 18.2967 180.46 22.5908 178.705L175.424 119.026C186.354 114.735 186.354 99.3276 175.424 95.0369L70.2169 53.4955Z" fill="black" />
|
||||
<path d="M297.03 168.968C301.519 171.893 307.57 169.358 308.351 164.092C310.303 150.05 312.06 125.866 304.057 107.338C293.321 82.9591 274.974 67.7468 266.19 61.7008C263.458 59.7505 259.749 59.9456 257.212 62.2859L218.564 96.4162C212.318 102.072 212.904 112.019 219.931 116.699L297.03 168.968Z" fill="black" />
|
||||
<path d="M189.089 299.428C188.699 303.914 192.603 307.814 197.092 307.229C211.731 305.669 241.79 299.818 264.237 278.365C286.098 257.496 293.32 232.728 295.272 222.781C295.858 220.051 295.272 217.32 293.515 215.175L225.98 131.897C218.758 122.925 204.119 127.411 203.143 138.918L189.089 299.233V299.428Z" fill="black" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
40
src/pages/sidebar/components/Sidebar.tsx
Normal file
40
src/pages/sidebar/components/Sidebar.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { FolderKanban, LayoutDashboard, Settings, PlayCircle, Cloud } from 'lucide-react'
|
||||
import { Sidebar, type NavItem } from '@/components/a/Sidebar'
|
||||
import { Logo } from './CNBBlackLogo.tsx'
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{
|
||||
title: '仓库管理',
|
||||
path: '/',
|
||||
icon: <FolderKanban className="w-5 h-5" />,
|
||||
},
|
||||
{
|
||||
title: '云端环境',
|
||||
path: '/cloud-env',
|
||||
icon: <Cloud className="w-5 h-5" />,
|
||||
},
|
||||
{
|
||||
title: '工作空间',
|
||||
path: '/workspaces',
|
||||
icon: <LayoutDashboard className="w-5 h-5" />,
|
||||
},
|
||||
{
|
||||
title: '应用配置',
|
||||
path: '/config',
|
||||
icon: <Settings className="w-5 h-5" />,
|
||||
},
|
||||
{
|
||||
title: '其他',
|
||||
path: '/demo',
|
||||
icon: <PlayCircle className="w-5 h-5" />,
|
||||
isDeveloping: true,
|
||||
},
|
||||
]
|
||||
|
||||
export function SidebarLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<Sidebar items={navItems} title='云原生' logo={<Logo className='w-6 h-6' />}>
|
||||
{children}
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
1
src/pages/sidebar/components/index.ts
Normal file
1
src/pages/sidebar/components/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { SidebarLayout } from './Sidebar'
|
||||
105
src/pages/workspaces/components/CreateDialog.tsx
Normal file
105
src/pages/workspaces/components/CreateDialog.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { TagInput } from "./TagInput";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
export function CreateDialog({ open, onClose, onSubmit }: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: { title: string, tags?: string[], link?: string, summary?: string, description?: string }) => Promise<void>;
|
||||
}) {
|
||||
const [title, setTitle] = useState('');
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
const [link, setLink] = useState('');
|
||||
const [summary, setSummary] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!title.trim()) {
|
||||
alert('请输入标题');
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await onSubmit({
|
||||
title: title.trim(),
|
||||
tags,
|
||||
link: link.trim(),
|
||||
summary: summary.trim(),
|
||||
description: description.trim(),
|
||||
});
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent className="max-w-lg!">
|
||||
<DialogHeader>
|
||||
<DialogTitle>创建Workspace</DialogTitle>
|
||||
<DialogDescription>填写以下信息创建一个新的Workspace</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="create-title">标题</Label>
|
||||
<Input
|
||||
id="create-title"
|
||||
value={title}
|
||||
onChange={e => setTitle(e.target.value)}
|
||||
placeholder="请输入标题"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>标签</Label>
|
||||
<TagInput value={tags} onChange={setTags} placeholder="输入标签后按回车添加" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="create-link">链接</Label>
|
||||
<Input
|
||||
id="create-link"
|
||||
value={link}
|
||||
onChange={e => setLink(e.target.value)}
|
||||
placeholder="https://..."
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="create-summary">摘要</Label>
|
||||
<Input
|
||||
id="create-summary"
|
||||
value={summary}
|
||||
onChange={e => setSummary(e.target.value)}
|
||||
placeholder="简要描述"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="create-description">描述</Label>
|
||||
<Textarea
|
||||
id="create-description"
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
placeholder="详细描述..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose} disabled={submitting}>取消</Button>
|
||||
<Button variant="outline" onClick={handleSubmit} disabled={submitting}>
|
||||
{submitting ? '创建中...' : '创建'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
117
src/pages/workspaces/components/EditDialog.tsx
Normal file
117
src/pages/workspaces/components/EditDialog.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { TagInput } from "./TagInput";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
export function EditDialog({ open, item, onClose, onSubmit }: {
|
||||
open: boolean;
|
||||
item: any;
|
||||
onClose: () => void;
|
||||
onSubmit: (id: string, data: { title?: string, tags?: string[], link?: string, summary?: string, description?: string }) => Promise<void>;
|
||||
}) {
|
||||
const [title, setTitle] = useState('');
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
const [link, setLink] = useState('');
|
||||
const [summary, setSummary] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && item) {
|
||||
setTitle(item.title || '');
|
||||
setTags(item.tags || []);
|
||||
setLink(item.link || '');
|
||||
setSummary(item.summary || '');
|
||||
setDescription(item.description || '');
|
||||
}
|
||||
}, [open, item]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!title.trim()) {
|
||||
alert('请输入标题');
|
||||
return;
|
||||
}
|
||||
if (!item?.id) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await onSubmit(item.id, {
|
||||
title: title.trim(),
|
||||
tags,
|
||||
link: link.trim(),
|
||||
summary: summary.trim(),
|
||||
description: description.trim(),
|
||||
});
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent className="max-w-lg!">
|
||||
<DialogHeader>
|
||||
<DialogTitle>编辑Workspace</DialogTitle>
|
||||
<DialogDescription>修改Workspace信息</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-title">标题</Label>
|
||||
<Input
|
||||
id="edit-title"
|
||||
value={title}
|
||||
onChange={e => setTitle(e.target.value)}
|
||||
placeholder="请输入标题"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>标签</Label>
|
||||
<TagInput value={tags} onChange={setTags} placeholder="输入标签后按回车添加" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-link">链接</Label>
|
||||
<Input
|
||||
id="edit-link"
|
||||
value={link}
|
||||
onChange={e => setLink(e.target.value)}
|
||||
placeholder="https://..."
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-summary">摘要</Label>
|
||||
<Input
|
||||
id="edit-summary"
|
||||
value={summary}
|
||||
onChange={e => setSummary(e.target.value)}
|
||||
placeholder="简要描述"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-description">描述</Label>
|
||||
<Textarea
|
||||
id="edit-description"
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
placeholder="详细描述..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose} disabled={submitting}>取消</Button>
|
||||
<Button variant="outline" onClick={handleSubmit} disabled={submitting}>
|
||||
{submitting ? '保存中...' : '保存'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
53
src/pages/workspaces/components/TagInput.tsx
Normal file
53
src/pages/workspaces/components/TagInput.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { useState } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { XIcon } from "lucide-react";
|
||||
|
||||
export function TagInput({ value, onChange, placeholder }: {
|
||||
value: string[];
|
||||
onChange: (tags: string[]) => void;
|
||||
placeholder?: string;
|
||||
}) {
|
||||
const [input, setInput] = useState('');
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && input.trim()) {
|
||||
e.preventDefault();
|
||||
if (!value.includes(input.trim())) {
|
||||
onChange([...value, input.trim()]);
|
||||
}
|
||||
setInput('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = (tag: string) => {
|
||||
onChange(value.filter(t => t !== tag));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder || '输入标签后按回车添加'}
|
||||
/>
|
||||
{value.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{value.map((tag) => (
|
||||
<Badge key={tag} variant="outline" className="gap-1">
|
||||
{tag}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemove(tag)}
|
||||
className="hover:text-destructive"
|
||||
>
|
||||
<XIcon className="size-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
src/pages/workspaces/components/index.ts
Normal file
3
src/pages/workspaces/components/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { CreateDialog } from "./CreateDialog";
|
||||
export { EditDialog } from "./EditDialog";
|
||||
export { TagInput } from "./TagInput";
|
||||
153
src/pages/workspaces/page.tsx
Normal file
153
src/pages/workspaces/page.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { useWorkspaceStore, type WorkspaceState } from "./store";
|
||||
import { useShallow } from "zustand/shallow";
|
||||
import { useEffect, useState } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { CreateDialog, EditDialog } from "./components";
|
||||
import { SearchIcon, RefreshCwIcon, PlusIcon, PencilIcon, TrashIcon } from "lucide-react";
|
||||
import { SidebarLayout } from "@/pages/sidebar/components";
|
||||
|
||||
export const App = () => {
|
||||
const workspaceStore = useWorkspaceStore(useShallow((state: WorkspaceState) => {
|
||||
return {
|
||||
list: state.list,
|
||||
loading: state.loading,
|
||||
getList: state.getList,
|
||||
createItem: state.createItem,
|
||||
updateItem: state.updateItem,
|
||||
deleteItem: state.deleteItem,
|
||||
showCreateDialog: state.showCreateDialog,
|
||||
setShowCreateDialog: state.setShowCreateDialog,
|
||||
showEditDialog: state.showEditDialog,
|
||||
setShowEditDialog: state.setShowEditDialog,
|
||||
editingItem: state.editingItem,
|
||||
setEditingItem: state.setEditingItem,
|
||||
}
|
||||
}));
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
workspaceStore.getList({ search });
|
||||
}, [search]);
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (confirm('确定要删除这个workspace吗?')) {
|
||||
await workspaceStore.deleteItem(id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
workspaceStore.getList({ search });
|
||||
};
|
||||
|
||||
const handleEdit = (item: any) => {
|
||||
workspaceStore.setEditingItem(item);
|
||||
workspaceStore.setShowEditDialog(true);
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
workspaceStore.setShowCreateDialog(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<SidebarLayout>
|
||||
<div className="p-5">
|
||||
<div className="flex justify-between items-center mb-5">
|
||||
<h1 className="text-2xl font-semibold">Workspaces</h1>
|
||||
<div className="flex gap-2">
|
||||
<div className="relative">
|
||||
<SearchIcon className="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="搜索workspace..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="pl-8 w-48"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={handleRefresh}>
|
||||
<RefreshCwIcon className="size-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleCreate}>
|
||||
<PlusIcon className="size-4" />
|
||||
创建Workspace
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{workspaceStore.loading ? (
|
||||
<div className="text-center py-10 text-muted-foreground">加载中...</div>
|
||||
) : workspaceStore.list.length === 0 ? (
|
||||
<div className="text-center py-10 text-muted-foreground">暂无workspace数据</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{workspaceStore.list.map((item) => (
|
||||
<Card key={item.id}>
|
||||
<CardHeader>
|
||||
<CardTitle>{item.title || '未命名'}</CardTitle>
|
||||
<CardDescription className="text-xs">ID: {item.id}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{item.tags && item.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{item.tags.map((tag, index) => (
|
||||
<Badge key={index} variant="outline">{tag}</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{item.summary && (
|
||||
<p className="text-sm text-muted-foreground">{item.summary}</p>
|
||||
)}
|
||||
{item.description && (
|
||||
<p className="text-sm text-muted-foreground line-clamp-2">{item.description}</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
创建时间: {item.createdAt ? dayjs(item.createdAt).format('YYYY-MM-DD HH:mm:ss') : '-'}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
更新时间: {item.updatedAt ? dayjs(item.updatedAt).format('YYYY-MM-DD HH:mm:ss') : '-'}
|
||||
</p>
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button variant="outline" size="sm" onClick={() => handleEdit(item)}>
|
||||
<PencilIcon className="size-3.5" />
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => handleDelete(item.id)}>
|
||||
<TrashIcon className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CreateDialog
|
||||
open={workspaceStore.showCreateDialog}
|
||||
onClose={() => workspaceStore.setShowCreateDialog(false)}
|
||||
onSubmit={workspaceStore.createItem}
|
||||
/>
|
||||
|
||||
<EditDialog
|
||||
open={workspaceStore.showEditDialog}
|
||||
item={workspaceStore.editingItem}
|
||||
onClose={() => {
|
||||
workspaceStore.setShowEditDialog(false);
|
||||
workspaceStore.setEditingItem(null);
|
||||
}}
|
||||
onSubmit={workspaceStore.updateItem}
|
||||
/>
|
||||
</div>
|
||||
</SidebarLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
156
src/pages/workspaces/store/index.ts
Normal file
156
src/pages/workspaces/store/index.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { useMarkStore } from '@kevisual/api/store-mark';
|
||||
export { useMarkStore }
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { queryApi } from '@/modules/mark-api';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
type WorkspaceItem = {
|
||||
id: string;
|
||||
title: string;
|
||||
tags?: string[];
|
||||
link?: string;
|
||||
summary?: string;
|
||||
description?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
type WorkspaceState = {
|
||||
edit: boolean;
|
||||
setEdit: (edit: boolean) => void;
|
||||
list: WorkspaceItem[];
|
||||
loading: boolean;
|
||||
setLoading: (loading: boolean) => void;
|
||||
// 弹窗状态
|
||||
showCreateDialog: boolean;
|
||||
setShowCreateDialog: (show: boolean) => void;
|
||||
showEditDialog: boolean;
|
||||
setShowEditDialog: (show: boolean) => void;
|
||||
editingItem: WorkspaceItem | null;
|
||||
setEditingItem: (item: WorkspaceItem | null) => void;
|
||||
// 数据操作
|
||||
getList: (params?: { search?: string, page?: number, pageSize?: number }) => Promise<void>;
|
||||
createItem: (data: { title: string, tags?: string[], link?: string, summary?: string, description?: string }) => Promise<void>;
|
||||
updateItem: (id: string, data: { title?: string, tags?: string[], link?: string, summary?: string, description?: string }) => Promise<void>;
|
||||
deleteItem: (id: string) => Promise<void>;
|
||||
getItem: (id: string) => Promise<WorkspaceItem | null>;
|
||||
}
|
||||
|
||||
export type { WorkspaceState, WorkspaceItem };
|
||||
|
||||
export const useWorkspaceStore = create<WorkspaceState>((set, get) => ({
|
||||
edit: false,
|
||||
setEdit: (edit) => set({ edit }),
|
||||
list: [],
|
||||
loading: false,
|
||||
setLoading: (loading) => set({ loading }),
|
||||
showCreateDialog: false,
|
||||
setShowCreateDialog: (show) => set({ showCreateDialog: show }),
|
||||
showEditDialog: false,
|
||||
setShowEditDialog: (show) => set({ showEditDialog: show }),
|
||||
editingItem: null,
|
||||
setEditingItem: (item) => set({ editingItem: item }),
|
||||
|
||||
getList: async (params = {}) => {
|
||||
const { page = 1, pageSize = 20, search } = params;
|
||||
set({ loading: true });
|
||||
try {
|
||||
const res = await queryApi.mark.list({
|
||||
markType: 'cnb',
|
||||
page,
|
||||
pageSize,
|
||||
search,
|
||||
sort: 'DESC'
|
||||
});
|
||||
if (res.code === 200) {
|
||||
set({ list: res.data?.list || [] });
|
||||
} else {
|
||||
toast.error(res.message || '获取列表失败');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取workspace列表失败', e);
|
||||
toast.error('获取列表失败');
|
||||
} finally {
|
||||
set({ loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
createItem: async (data) => {
|
||||
try {
|
||||
const res = await queryApi.mark.create({
|
||||
title: data.title,
|
||||
markType: 'cnb',
|
||||
tags: data.tags || [],
|
||||
link: data.link || '',
|
||||
summary: data.summary || '',
|
||||
description: data.description || ''
|
||||
});
|
||||
if (res.code === 200) {
|
||||
toast.success('创建成功');
|
||||
get().getList();
|
||||
set({ showCreateDialog: false });
|
||||
} else {
|
||||
toast.error(res.message || '创建失败');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('创建失败', e);
|
||||
toast.error('创建失败');
|
||||
}
|
||||
},
|
||||
|
||||
updateItem: async (id, data) => {
|
||||
try {
|
||||
const res = await queryApi.mark.update({
|
||||
id,
|
||||
data: {
|
||||
title: data.title || '',
|
||||
tags: data.tags || [],
|
||||
link: data.link || '',
|
||||
summary: data.summary || '',
|
||||
description: data.description || ''
|
||||
}
|
||||
});
|
||||
if (res.code === 200) {
|
||||
toast.success('更新成功');
|
||||
get().getList();
|
||||
set({ showEditDialog: false, editingItem: null });
|
||||
} else {
|
||||
toast.error(res.message || '更新失败');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('更新失败', e);
|
||||
toast.error('更新失败');
|
||||
}
|
||||
},
|
||||
|
||||
deleteItem: async (id) => {
|
||||
try {
|
||||
const res = await queryApi.mark.delete({ id });
|
||||
if (res.code === 200) {
|
||||
toast.success('删除成功');
|
||||
get().getList();
|
||||
} else {
|
||||
toast.error(res.message || '删除失败');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('删除失败', e);
|
||||
toast.error('删除失败');
|
||||
}
|
||||
},
|
||||
|
||||
getItem: async (id) => {
|
||||
try {
|
||||
const res = await queryApi.mark.get({ id });
|
||||
if (res.code === 200) {
|
||||
return res.data;
|
||||
} else {
|
||||
toast.error(res.message || '获取详情失败');
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取详情失败', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}));
|
||||
@@ -9,38 +9,147 @@
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as LoginRouteImport } from './routes/login'
|
||||
import { Route as DemoRouteImport } from './routes/demo'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as WorkspacesIndexRouteImport } from './routes/workspaces/index'
|
||||
import { Route as RepoIndexRouteImport } from './routes/repo/index'
|
||||
import { Route as ConfigIndexRouteImport } from './routes/config/index'
|
||||
import { Route as CloudEnvIndexRouteImport } from './routes/cloud-env/index'
|
||||
import { Route as ConfigGiteaRouteImport } from './routes/config/gitea'
|
||||
|
||||
const LoginRoute = LoginRouteImport.update({
|
||||
id: '/login',
|
||||
path: '/login',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const DemoRoute = DemoRouteImport.update({
|
||||
id: '/demo',
|
||||
path: '/demo',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const WorkspacesIndexRoute = WorkspacesIndexRouteImport.update({
|
||||
id: '/workspaces/',
|
||||
path: '/workspaces/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const RepoIndexRoute = RepoIndexRouteImport.update({
|
||||
id: '/repo/',
|
||||
path: '/repo/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ConfigIndexRoute = ConfigIndexRouteImport.update({
|
||||
id: '/config/',
|
||||
path: '/config/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const CloudEnvIndexRoute = CloudEnvIndexRouteImport.update({
|
||||
id: '/cloud-env/',
|
||||
path: '/cloud-env/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ConfigGiteaRoute = ConfigGiteaRouteImport.update({
|
||||
id: '/config/gitea',
|
||||
path: '/config/gitea',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/demo': typeof DemoRoute
|
||||
'/login': typeof LoginRoute
|
||||
'/config/gitea': typeof ConfigGiteaRoute
|
||||
'/cloud-env/': typeof CloudEnvIndexRoute
|
||||
'/config/': typeof ConfigIndexRoute
|
||||
'/repo/': typeof RepoIndexRoute
|
||||
'/workspaces/': typeof WorkspacesIndexRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/demo': typeof DemoRoute
|
||||
'/login': typeof LoginRoute
|
||||
'/config/gitea': typeof ConfigGiteaRoute
|
||||
'/cloud-env': typeof CloudEnvIndexRoute
|
||||
'/config': typeof ConfigIndexRoute
|
||||
'/repo': typeof RepoIndexRoute
|
||||
'/workspaces': typeof WorkspacesIndexRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/demo': typeof DemoRoute
|
||||
'/login': typeof LoginRoute
|
||||
'/config/gitea': typeof ConfigGiteaRoute
|
||||
'/cloud-env/': typeof CloudEnvIndexRoute
|
||||
'/config/': typeof ConfigIndexRoute
|
||||
'/repo/': typeof RepoIndexRoute
|
||||
'/workspaces/': typeof WorkspacesIndexRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/'
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/demo'
|
||||
| '/login'
|
||||
| '/config/gitea'
|
||||
| '/cloud-env/'
|
||||
| '/config/'
|
||||
| '/repo/'
|
||||
| '/workspaces/'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/'
|
||||
id: '__root__' | '/'
|
||||
to:
|
||||
| '/'
|
||||
| '/demo'
|
||||
| '/login'
|
||||
| '/config/gitea'
|
||||
| '/cloud-env'
|
||||
| '/config'
|
||||
| '/repo'
|
||||
| '/workspaces'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/demo'
|
||||
| '/login'
|
||||
| '/config/gitea'
|
||||
| '/cloud-env/'
|
||||
| '/config/'
|
||||
| '/repo/'
|
||||
| '/workspaces/'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
DemoRoute: typeof DemoRoute
|
||||
LoginRoute: typeof LoginRoute
|
||||
ConfigGiteaRoute: typeof ConfigGiteaRoute
|
||||
CloudEnvIndexRoute: typeof CloudEnvIndexRoute
|
||||
ConfigIndexRoute: typeof ConfigIndexRoute
|
||||
RepoIndexRoute: typeof RepoIndexRoute
|
||||
WorkspacesIndexRoute: typeof WorkspacesIndexRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/login': {
|
||||
id: '/login'
|
||||
path: '/login'
|
||||
fullPath: '/login'
|
||||
preLoaderRoute: typeof LoginRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/demo': {
|
||||
id: '/demo'
|
||||
path: '/demo'
|
||||
fullPath: '/demo'
|
||||
preLoaderRoute: typeof DemoRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
@@ -48,11 +157,53 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/workspaces/': {
|
||||
id: '/workspaces/'
|
||||
path: '/workspaces'
|
||||
fullPath: '/workspaces/'
|
||||
preLoaderRoute: typeof WorkspacesIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/repo/': {
|
||||
id: '/repo/'
|
||||
path: '/repo'
|
||||
fullPath: '/repo/'
|
||||
preLoaderRoute: typeof RepoIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/config/': {
|
||||
id: '/config/'
|
||||
path: '/config'
|
||||
fullPath: '/config/'
|
||||
preLoaderRoute: typeof ConfigIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/cloud-env/': {
|
||||
id: '/cloud-env/'
|
||||
path: '/cloud-env'
|
||||
fullPath: '/cloud-env/'
|
||||
preLoaderRoute: typeof CloudEnvIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/config/gitea': {
|
||||
id: '/config/gitea'
|
||||
path: '/config/gitea'
|
||||
fullPath: '/config/gitea'
|
||||
preLoaderRoute: typeof ConfigGiteaRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
DemoRoute: DemoRoute,
|
||||
LoginRoute: LoginRoute,
|
||||
ConfigGiteaRoute: ConfigGiteaRoute,
|
||||
CloudEnvIndexRoute: CloudEnvIndexRoute,
|
||||
ConfigIndexRoute: ConfigIndexRoute,
|
||||
RepoIndexRoute: RepoIndexRoute,
|
||||
WorkspacesIndexRoute: WorkspacesIndexRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
|
||||
@@ -1,27 +1,40 @@
|
||||
import { Link, Outlet, createRootRoute } from '@tanstack/react-router'
|
||||
import { LayoutMain } from '@/pages/auth/modules/BaseHeader';
|
||||
import { Outlet, createRootRoute } from '@tanstack/react-router'
|
||||
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
|
||||
|
||||
import { Toaster } from '@/components/ui/sonner'
|
||||
import { AuthProvider } from '@/pages/auth'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import { useLayoutStore } from '@/pages/auth/store';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
import { stackQueryClient } from '@/modules/query'
|
||||
import { QueryClientProvider } from '@tanstack/react-query'
|
||||
import clsx from 'clsx';
|
||||
export const Route = createRootRoute({
|
||||
component: RootComponent,
|
||||
})
|
||||
|
||||
|
||||
function RootComponent() {
|
||||
const store = useLayoutStore(useShallow(state => ({
|
||||
showBaseHeader: state.showBaseHeader,
|
||||
})));
|
||||
return (
|
||||
<>
|
||||
<div className="p-2 flex gap-2 text-lg">
|
||||
<Link
|
||||
to="/"
|
||||
activeProps={{
|
||||
className: 'font-bold',
|
||||
}}
|
||||
activeOptions={{ exact: true }}
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
<QueryClientProvider client={stackQueryClient}>
|
||||
<div className='h-full overflow-hidden'>
|
||||
<LayoutMain />
|
||||
<AuthProvider mustLogin={false}>
|
||||
<TooltipProvider>
|
||||
<main className={clsx('overflow-auto scrollbar', {
|
||||
'h-[calc(100%-3rem)]': store.showBaseHeader,
|
||||
'h-full': !store.showBaseHeader,
|
||||
})}>
|
||||
<Outlet />
|
||||
</main>
|
||||
</TooltipProvider>
|
||||
</AuthProvider>
|
||||
<TanStackRouterDevtools position="bottom-right" />
|
||||
<Toaster />
|
||||
</div>
|
||||
<hr />
|
||||
<Outlet />
|
||||
<TanStackRouterDevtools position="bottom-right" />
|
||||
</>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
10
src/routes/cloud-env/index.tsx
Normal file
10
src/routes/cloud-env/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import App from '@/pages/cloud-env/page'
|
||||
|
||||
export const Route = createFileRoute('/cloud-env/')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <App />
|
||||
}
|
||||
9
src/routes/config/gitea.tsx
Normal file
9
src/routes/config/gitea.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import App from '@/pages/config/gitea/page'
|
||||
export const Route = createFileRoute('/config/gitea')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <App />
|
||||
}
|
||||
9
src/routes/config/index.tsx
Normal file
9
src/routes/config/index.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import App from '@/pages/config/page'
|
||||
export const Route = createFileRoute('/config/')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <App />
|
||||
}
|
||||
9
src/routes/demo.tsx
Normal file
9
src/routes/demo.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import App from '@/pages/demo/page'
|
||||
export const Route = createFileRoute('/demo')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <App />
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { Home } from '@/pages/Home'
|
||||
import App from '@/pages/page'
|
||||
export const Route = createFileRoute('/')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <Home />
|
||||
return <App />
|
||||
}
|
||||
9
src/routes/login.tsx
Normal file
9
src/routes/login.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import App from '@/pages/auth/page'
|
||||
export const Route = createFileRoute('/login')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <App />
|
||||
}
|
||||
9
src/routes/repo/index.tsx
Normal file
9
src/routes/repo/index.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import App from '@/pages/repos/repo/page'
|
||||
export const Route = createFileRoute('/repo/')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <App />
|
||||
}
|
||||
9
src/routes/workspaces/index.tsx
Normal file
9
src/routes/workspaces/index.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import App from '@/pages/workspaces/page'
|
||||
export const Route = createFileRoute('/workspaces/')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <App />
|
||||
}
|
||||
17
test/ai.ts
Normal file
17
test/ai.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
||||
import { generateText } from 'ai';
|
||||
|
||||
|
||||
const cnb = createOpenAICompatible({
|
||||
baseURL: 'https://api.cnb.cool/kevisual/cnb-ai/-/ai',
|
||||
name: 'custom-cnb',
|
||||
apiKey: 'cIDfLOOIr1Trt15cdnwfndupEZG',
|
||||
});
|
||||
// const model = config.AI_MODEL;
|
||||
const model = 'hunyuan';
|
||||
const { text } = await generateText({
|
||||
model: cnb(model),
|
||||
prompt: 'Say hello in one sentence.',
|
||||
});
|
||||
console.log('text', text)
|
||||
// https://api.cnb.cool
|
||||
@@ -4,11 +4,14 @@ import path from 'path';
|
||||
import pkgs from './package.json';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { tanstackRouter } from '@tanstack/router-plugin/vite'
|
||||
import dotenv from 'dotenv';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const env = dotenv.config().parsed || {};
|
||||
const isDev = env.NODE_ENV === 'development' || process.env.NODE_ENV === 'development';
|
||||
const basename = isDev ? '/' : pkgs?.basename || '/';
|
||||
|
||||
let target = process.env.VITE_API_URL || 'http://localhost:51515';
|
||||
let target = env.VITE_API_URL || process.env.API_URL || 'http://localhost:51515';
|
||||
const apiProxy = { target: target, changeOrigin: true, ws: true, rewriteWsOrigin: true, secure: false, cookieDomainRewrite: 'localhost' };
|
||||
let proxy = {
|
||||
'/root/': apiProxy,
|
||||
@@ -26,7 +29,10 @@ export default defineConfig({
|
||||
autoCodeSplitting: true,
|
||||
}),
|
||||
react(),
|
||||
tailwindcss()
|
||||
tailwindcss(),
|
||||
VitePWA({
|
||||
injectRegister: 'auto',
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
Reference in New Issue
Block a user