commit 8fdc7ac0c3b2b52c8c2517373c0621b81e704bc1 Author: 刘 相卿 Date: Wed Apr 8 21:26:18 2026 +0800 all diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..45bd807 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# EditorConfig is awesome: https://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[Makefile] +indent_style = tab diff --git a/.gitea/actions/get-version/action.yaml b/.gitea/actions/get-version/action.yaml new file mode 100644 index 0000000..263188c --- /dev/null +++ b/.gitea/actions/get-version/action.yaml @@ -0,0 +1,60 @@ +name: Get Version +description: 从 package.json 读取版本号,与最新 tag 对比决定使用哪个版本 + +outputs: + version: + description: 最终确定的版本号 (带 v 前缀) + value: ${{ steps.get_version.outputs.version }} + +runs: + using: composite + steps: + - name: Get version from package.json + id: get_version + shell: bash + run: | + PKG_VERSION=$(node -p "require('./package.json').version") + echo "Package.json version: ${PKG_VERSION}" + + # 获取最新的tag(按版本号排序) + LATEST_TAG=$(git ls-remote --tags origin 'refs/tags/v*' 2>/dev/null | sed 's/.*refs\/tags\///' | sed 's/\^{}//' | sort -V | tail -n 1 || echo "") + echo "Latest tag: ${LATEST_TAG}" + + # 版本比较函数 + version_compare() { + # 返回: 0 = 相等, 1 = v1 > v2, 2 = v1 < v2 + if [[ "$1" == "$2" ]]; then echo 0; return; fi + local IFS=. + local i v1=($1) v2=($2) + for ((i=0; i<${#v1[@]} || i<${#v2[@]}; i++)); do + local n1=${v1[i]:-0} n2=${v2[i]:-0} + if ((n1 > n2)); then echo 1; return; fi + if ((n1 < n2)); then echo 2; return; fi + done + echo 0 + } + + if [[ -z "${LATEST_TAG}" ]]; then + # 没有任何tag,直接使用package.json版本 + NEW_TAG="v${PKG_VERSION}" + echo "No existing tags, using package.json version: ${NEW_TAG}" + else + # 去掉v前缀进行比较 + LATEST_VERSION="${LATEST_TAG#v}" + COMPARE_RESULT=$(version_compare "${PKG_VERSION}" "${LATEST_VERSION}") + + if [[ "${COMPARE_RESULT}" == "1" ]]; then + # package.json版本更大,使用它 + NEW_TAG="v${PKG_VERSION}" + echo "Package.json version is greater, using: ${NEW_TAG}" + else + # 最新tag版本更大或相等,在其基础上递增patch + IFS='.' read -r MAJOR MINOR PATCH <<< "${LATEST_VERSION}" + PATCH=$((PATCH + 1)) + NEW_TAG="v${MAJOR}.${MINOR}.${PATCH}" + echo "Incrementing from latest tag, new version: ${NEW_TAG}" + fi + fi + + echo "version=${NEW_TAG}" >> $GITHUB_OUTPUT + echo "Final Version: ${NEW_TAG}" diff --git a/.gitea/actions/wecom-notification/action.yaml b/.gitea/actions/wecom-notification/action.yaml new file mode 100644 index 0000000..f4d1b55 --- /dev/null +++ b/.gitea/actions/wecom-notification/action.yaml @@ -0,0 +1,124 @@ +name: WeChat Work Notification +description: 发送企业微信群机器人通知 +author: Your Team + +inputs: + webhook_url: + description: 企业微信群机器人 Webhook URL + required: true + status: + description: 构建状态 (success/failure/cancelled) + required: true + default: success + title: + description: 通知标题 + required: true + repository: + description: 仓库名称 + required: false + default: ${{ github.repository }} + branch: + description: 分支名称 + required: false + default: '' + version: + description: 版本号 + required: false + default: '' + actor: + description: 构建触发人 + required: false + default: ${{ github.actor }} + run_number: + description: 运行序号 + required: false + default: ${{ github.run_number }} + run_url: + description: 运行详情 URL + required: false + default: '' + failed_step: + description: 失败的步骤名称 + required: false + default: '' + extra_content: + description: 额外的通知内容 (markdown 格式) + required: false + default: '' + +runs: + using: composite + steps: + - name: Send WeChat Work Notification + shell: bash + run: | + # 设置状态图标和文本 + if [ "${{ inputs.status }}" = "success" ]; then + STATUS_ICON="✅" + STATUS_TEXT="成功" + elif [ "${{ inputs.status }}" = "cancelled" ]; then + STATUS_ICON="⚠️" + STATUS_TEXT="已取消" + else + STATUS_ICON="❌" + STATUS_TEXT="失败" + fi + + # 构建通知内容 + CONTENT="## ${STATUS_ICON} ${{ inputs.title }} ${STATUS_TEXT}" + + # 添加仓库信息 + if [ -n "${{ inputs.repository }}" ]; then + CONTENT="${CONTENT}\n> **仓库**: ${{ inputs.repository }}" + fi + + # 添加分支信息 + if [ -n "${{ inputs.branch }}" ]; then + CONTENT="${CONTENT}\n> **分支**: ${{ inputs.branch }}" + fi + + # 添加版本信息 + if [ -n "${{ inputs.version }}" ]; then + CONTENT="${CONTENT}\n> **版本**: ${{ inputs.version }}" + fi + + # 添加构建人 + if [ -n "${{ inputs.actor }}" ]; then + CONTENT="${CONTENT}\n> **构建人**: ${{ inputs.actor }}" + fi + + # 添加运行序号 + if [ -n "${{ inputs.run_number }}" ]; then + CONTENT="${CONTENT}\n> **运行序号**: #${{ inputs.run_number }}" + fi + + # 添加失败步骤信息 + if [ -n "${{ inputs.failed_step }}" ]; then + CONTENT="${CONTENT}\n> **失败步骤**: ${{ inputs.failed_step }}" + fi + + # 添加额外内容 + if [ -n "${{ inputs.extra_content }}" ]; then + CONTENT="${CONTENT}\n> ${{ inputs.extra_content }}" + fi + + # 添加查看详情链接 + if [ -n "${{ inputs.run_url }}" ]; then + CONTENT="${CONTENT}\n> [查看详情](${{ inputs.run_url }})" + fi + + echo "========== 企业微信通知 ==========" + echo "状态: ${STATUS_ICON} ${STATUS_TEXT}" + echo "标题: ${{ inputs.title }}" + echo "Webhook URL: ${{ inputs.webhook_url }}" + echo "==================================" + + # 发送通知 + curl -s -X POST "${{ inputs.webhook_url }}" \ + -H "Content-Type: application/json" \ + -d "{ + \"msgtype\": \"markdown\", + \"markdown\": { + \"content\": \"${CONTENT}\" + } + }" diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml new file mode 100644 index 0000000..9b43999 --- /dev/null +++ b/.gitea/workflows/deploy-dev.yaml @@ -0,0 +1,58 @@ +name: Deploy Dev + +on: + workflow_dispatch: + inputs: + # 分支 + target_branch: + description: 请输入要发布的分支名称 + required: true + default: dev + type: string + +jobs: + build-and-deploy: + runs-on: node-22 + + steps: + - uses: https://gitee.com/youtellme/checkout@v4 + with: + ref: ${{ inputs.target_branch }} + fetch-depth: 1 + + - name: Setup pnpm + id: setup_pnpm + uses: https://gitee.com/youtellme/pnpm-action-setup@v4 + + - name: Install dependencies + id: install + run: | + rm -rf pnpm-lock.yaml node_modules + pnpm install + + - run: pnpm run build:web + + # 压缩 dist 文件夹 + - name: zip dist folder + run: | + TIMESTAMP=$(date +%Y%m%d%H%M%S) + BRANCH_NAME="${{ inputs.target_branch }}" + SAFE_BRANCH_NAME="${BRANCH_NAME//\//_}" + mkdir -p zip-output + cd apps/web/dist + zip -r -q ../../../zip-output/cslab-dcs-web_${SAFE_BRANCH_NAME}_${TIMESTAMP}.zip . + cd - + ls -lh zip-output/ + + # 上传 zip 文件到 RustFS + - name: Upload zip to RustFS (S3) + uses: jakejarvis/s3-sync-action@master + with: + args: --acl public-read + env: + AWS_S3_BUCKET: ${{ vars.RUSTFS_AWS_S3_BUCKET }} + AWS_ACCESS_KEY_ID: ${{ vars.RUSTFS_ACCESS_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ vars.RUSTFS_SECRET_KEY }} + AWS_REGION: us-east-1 + AWS_S3_ENDPOINT: ${{ vars.RUSTFS_AWS_S3_ENDPOINT }} + SOURCE_DIR: zip-output diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa9363d --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +# Dependencies +node_modules +.pnpm-store + +# Build outputs +dist +out +*.local + +# Electron +release + +# Tauri +**/src-tauri/target +**/src-tauri/gen + +# Logs +logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +!.vscode/settings.json +!.vscode/launch.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +*~ + +# Environment files + +# Test coverage +coverage +*.lcov + +# TypeScript cache +*.tsbuildinfo + +# Vite +.vite diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..1efb3ee --- /dev/null +++ b/.npmrc @@ -0,0 +1,3 @@ +shamefully-hoist=true +strict-peer-dependencies=false +auto-install-peers=true diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..940260d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["dbaeumer.vscode-eslint"] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..0b6b9a6 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,39 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Main Process", + "type": "node", + "request": "launch", + "cwd": "${workspaceRoot}", + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite", + "windows": { + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd" + }, + "runtimeArgs": ["--sourcemap"], + "env": { + "REMOTE_DEBUGGING_PORT": "9222" + } + }, + { + "name": "Debug Renderer Process", + "port": 9222, + "request": "attach", + "type": "chrome", + "webRoot": "${workspaceFolder}/src/renderer", + "timeout": 60000, + "presentation": { + "hidden": true + } + } + ], + "compounds": [ + { + "name": "Debug All", + "configurations": ["Debug Main Process", "Debug Renderer Process"], + "presentation": { + "order": 1 + } + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4b356f5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,50 @@ +{ + // Disable the default formatter, use eslint instead + "prettier.enable": false, + "editor.formatOnSave": false, + + // Auto fix + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.organizeImports": "never" + }, + + // Silent the stylistic rules in your IDE, but still auto fix them + "eslint.rules.customizations": [ + { "rule": "style/*", "severity": "off", "fixable": true }, + { "rule": "format/*", "severity": "off", "fixable": true }, + { "rule": "*-indent", "severity": "off", "fixable": true }, + { "rule": "*-spacing", "severity": "off", "fixable": true }, + { "rule": "*-spaces", "severity": "off", "fixable": true }, + { "rule": "*-order", "severity": "off", "fixable": true }, + { "rule": "*-dangle", "severity": "off", "fixable": true }, + { "rule": "*-newline", "severity": "off", "fixable": true }, + { "rule": "*quotes", "severity": "off", "fixable": true }, + { "rule": "*semi", "severity": "off", "fixable": true } + ], + + // Enable eslint for all supported languages + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact", + "vue", + "html", + "markdown", + "json", + "jsonc", + "yaml", + "toml", + "xml", + "gql", + "graphql", + "astro", + "svelte", + "css", + "less", + "scss", + "pcss", + "postcss" + ] +} diff --git a/Jenkinsfile.dev110 b/Jenkinsfile.dev110 new file mode 100644 index 0000000..8ec0dde --- /dev/null +++ b/Jenkinsfile.dev110 @@ -0,0 +1,139 @@ +pipeline { + agent any + + environment { + // 项目信息 + PROJECT_NAME = 'DCS 画布编辑器' + APP_NAME = 'cslab-dcs-web' + BUILD_DIR = 'apps/web/dist' + + // 部署目标 - 110开发环境 (内网 110) + SSH_SERVER = 'ssh-dev-110' + DEPLOY_HOST = '192.168.1.110' + REMOTE_DIR = 'D:/www/chemlab/jenkins-dir' + DEPLOY_BASE = 'D:/www/chemlab' + DEPLOY_DIR = "${DEPLOY_BASE}/${APP_NAME}" + + // 企业微信 Webhook(从 Jenkins 凭据读取) + WEBHOOK_URL = credentials('webhook-url') + + // Node 版本(需在 Jenkins 全局工具配置中添加同名 NodeJS 安装) + NODE_HOME = tool(name: 'NodeJS-22', type: 'nodejs') + PATH = "${NODE_HOME}/bin:${env.PATH}" + } + + options { + timeout(time: 15, unit: 'MINUTES') + disableConcurrentBuilds() + buildDiscarder(logRotator(numToKeepStr: '10')) + } + + stages { + stage('环境检查') { + steps { + echo "📋 项目: ${PROJECT_NAME}" + echo "🌿 分支: ${env.BRANCH_NAME ?: env.GIT_BRANCH ?: 'unknown'}" + echo "🔧 环境: 110开发 (${DEPLOY_HOST})" + sh ''' + echo "Node 版本: $(node -v)" + echo "pnpm 版本: $(pnpm -v)" + ''' + } + } + + stage('安装依赖') { + steps { + echo '📦 安装项目依赖...' + sh 'ELECTRON_SKIP_BINARY_DOWNLOAD=1 pnpm install' + } + } + + stage('构建') { + steps { + echo '🔨 构建110开发环境版本...' + sh 'pnpm build:web' + } + } + + stage('部署') { + steps { + echo "🚀 部署到110开发环境: ${DEPLOY_HOST}" + sshPublisher( + publishers: [ + sshPublisherDesc( + configName: SSH_SERVER, + transfers: [ + sshTransfer( + sourceFiles: "${BUILD_DIR}/**", + removePrefix: BUILD_DIR, + remoteDirectory: APP_NAME, + execCommand: """powershell -NoProfile -Command "Remove-Item '${DEPLOY_DIR}' -Recurse -Force -ErrorAction SilentlyContinue; Move-Item '${REMOTE_DIR}/${APP_NAME}' '${DEPLOY_DIR}'" + """ + ) + ] + ) + ] + ) + } + } + } + + post { + success { + echo '✅ 110开发环境部署成功!' + script { + def branch = env.BRANCH_NAME ?: env.GIT_BRANCH ?: 'unknown' + def timestamp = new Date().format('yyyy-MM-dd HH:mm:ss', TimeZone.getTimeZone('Asia/Shanghai')) + + def content = """## 🚀 DCS 画布编辑器 自动部署通知 110开发环境 + +**部署时间**: ${timestamp} + +**部署项目**: **${PROJECT_NAME}** + +**分支**: ${branch} + +**构建编号**: #${env.BUILD_NUMBER} + +> Jenkins 自动构建部署完成""" + + sh """ + curl -s -X POST \ + -H 'Content-Type: application/json' \ + -d '{"msgtype":"markdown","markdown":{"content":"${content}"}}' \ + '${WEBHOOK_URL}' || true + """ + } + } + failure { + echo '❌ 110开发环境部署失败!' + script { + def branch = env.BRANCH_NAME ?: env.GIT_BRANCH ?: 'unknown' + def timestamp = new Date().format('yyyy-MM-dd HH:mm:ss', TimeZone.getTimeZone('Asia/Shanghai')) + + def content = """## ❌ DCS 画布编辑器 部署失败通知 110开发环境 + +**时间**: ${timestamp} + +**项目**: **${PROJECT_NAME}** + +**分支**: ${branch} + +**构建编号**: [#${env.BUILD_NUMBER}](${env.BUILD_URL}) + +> 请检查 Jenkins 构建日志""" + + sh """ + curl -s -X POST \ + -H 'Content-Type: application/json' \ + -d '{"msgtype":"markdown","markdown":{"content":"${content}"}}' \ + '${WEBHOOK_URL}' || true + """ + } + } + always { + // 清理构建产物 + sh "rm -rf ${BUILD_DIR}" + } + } +} diff --git a/Jenkinsfile.dev139 b/Jenkinsfile.dev139 new file mode 100644 index 0000000..81e5bc9 --- /dev/null +++ b/Jenkinsfile.dev139 @@ -0,0 +1,155 @@ +pipeline { + agent any + + environment { + // 项目信息 + PROJECT_NAME = 'DCS 画布编辑器' + APP_NAME = 'cslab-dcs-web' + BUILD_DIR = 'apps/web/dist' + + // 部署目标 - 139测试环境 (内网 139) + SSH_SERVER = 'ssh-test-139' + DEPLOY_HOST = '192.168.1.139' + REMOTE_DIR = '/root/jenkins-dir' + DEPLOY_BASE = '/root/chemlab/www/chemlab' + DEPLOY_DIR = "${DEPLOY_BASE}/${APP_NAME}" + + // 企业微信 Webhook(从 Jenkins 凭据读取) + WEBHOOK_URL = credentials('webhook-url') + + // Node 版本(需在 Jenkins 全局工具配置中添加同名 NodeJS 安装) + NODE_HOME = tool(name: 'NodeJS-22', type: 'nodejs') + PATH = "${NODE_HOME}/bin:${env.PATH}" + } + + options { + timeout(time: 15, unit: 'MINUTES') + disableConcurrentBuilds() + buildDiscarder(logRotator(numToKeepStr: '10')) + } + + stages { + stage('环境检查') { + steps { + echo "📋 项目: ${PROJECT_NAME}" + echo "🌿 分支: ${env.BRANCH_NAME ?: env.GIT_BRANCH ?: 'unknown'}" + echo "🔧 环境: 139测试 (${DEPLOY_HOST})" + sh ''' + echo "Node 版本: $(node -v)" + echo "pnpm 版本: $(pnpm -v)" + ''' + } + } + + stage('安装依赖') { + steps { + echo '📦 安装项目依赖...' + sh 'ELECTRON_SKIP_BINARY_DOWNLOAD=1 pnpm install' + } + } + + stage('构建') { + steps { + echo '🔨 构建139测试环境版本...' + sh 'pnpm build:web' + } + } + + stage('部署') { + steps { + echo "🚀 部署到139测试环境: ${DEPLOY_HOST}" + sshPublisher( + publishers: [ + sshPublisherDesc( + configName: SSH_SERVER, + transfers: [ + sshTransfer( + sourceFiles: "${BUILD_DIR}/**", + removePrefix: BUILD_DIR, + remoteDirectory: APP_NAME, + execCommand: """ + TS=\$(date +%Y%m%d%H%M%S) + + # 备份旧版本 + if [ -d "${DEPLOY_DIR}" ]; then + mv "${DEPLOY_DIR}" "${DEPLOY_DIR}.backup.\$TS" + fi + mkdir -p "${DEPLOY_BASE}" + + # 移动到部署目录 + mv "${REMOTE_DIR}/${APP_NAME}" "${DEPLOY_DIR}" + + # 设置权限 + chmod -R 755 "${DEPLOY_DIR}" + + # 清理超过 7 天的旧备份 + find "${DEPLOY_BASE}" -maxdepth 1 -name "${APP_NAME}.backup.*" -mtime +7 -exec rm -rf {} + + """ + ) + ] + ) + ] + ) + } + } + } + + post { + success { + echo '✅ 139测试环境部署成功!' + script { + def branch = env.BRANCH_NAME ?: env.GIT_BRANCH ?: 'unknown' + def timestamp = new Date().format('yyyy-MM-dd HH:mm:ss', TimeZone.getTimeZone('Asia/Shanghai')) + + def content = """## 🚀 DCS 画布编辑器 自动部署通知 139测试环境 + +**部署时间**: ${timestamp} + +**部署项目**: **${PROJECT_NAME}** + +**分支**: ${branch} + +**构建编号**: #${env.BUILD_NUMBER} + +> Jenkins 自动构建部署完成""" + + sh """ + curl -s -X POST \ + -H 'Content-Type: application/json' \ + -d '{"msgtype":"markdown","markdown":{"content":"${content}"}}' \ + '${WEBHOOK_URL}' || true + """ + } + } + failure { + echo '❌ 139测试环境部署失败!' + script { + def branch = env.BRANCH_NAME ?: env.GIT_BRANCH ?: 'unknown' + def timestamp = new Date().format('yyyy-MM-dd HH:mm:ss', TimeZone.getTimeZone('Asia/Shanghai')) + + def content = """## ❌ DCS 画布编辑器 部署失败通知 139测试环境 + +**时间**: ${timestamp} + +**项目**: **${PROJECT_NAME}** + +**分支**: ${branch} + +**构建编号**: [#${env.BUILD_NUMBER}](${env.BUILD_URL}) + +> 请检查 Jenkins 构建日志""" + + sh """ + curl -s -X POST \ + -H 'Content-Type: application/json' \ + -d '{"msgtype":"markdown","markdown":{"content":"${content}"}}' \ + '${WEBHOOK_URL}' || true + """ + } + } + always { + // 清理构建产物 + sh "rm -rf ${BUILD_DIR}" + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..86aa8af --- /dev/null +++ b/README.md @@ -0,0 +1,463 @@ +# CSLAB-DCS-Web + +**DCS(分布式控制系统)跨平台可视化组态编辑器** + +面向工业控制场景的画布组态设计、数据绑定、实时仿真与运行监控平台,支持 Web、Electron、Tauri 三端部署。 + +--- + +## 目录 + +- [项目简介](#项目简介) +- [技术栈](#技术栈) +- [项目结构](#项目结构) +- [环境要求](#环境要求) +- [快速开始](#快速开始) +- [开发命令](#开发命令) +- [构建与部署](#构建与部署) +- [核心架构](#核心架构) +- [功能模块](#功能模块) +- [数据模型](#数据模型) +- [API 层](#api-层) +- [测试](#测试) +- [CI/CD](#cicd) + +--- + +## 项目简介 + +CSLAB-DCS-Web 是一个 **工业分布式控制系统可视化组态编辑器**,采用 pnpm monorepo 架构。用户可在画布上拖放、编排工业控制组件(如 PID 控制器、阀门控制器、仪表等),配置数据绑定与表达式,并通过 WebSocket 实现运行时的实时数据推送和仿真监控。 + +### 核心能力 + +- **画布组态设计**:基于可视化画布的工业控制界面设计,支持多图层、组件拖放、对齐辅助线、缩放漫游 +- **丰富组件库**:矩形、文本、数字显示、柱状图、按钮、PID 控制器、阀门控制器、画布切换器等 +- **数据绑定与表达式**:支持将组件属性绑定到运行时变量,内置表达式引擎(tokenizer → parser → evaluator) +- **条件样式**:根据运行时条件动态切换组件样式 +- **事件与动作**:组件支持 click/dblclick 事件触发,执行自定义动作 +- **实时运行监控**:通过 WebSocket 连接运行时服务,RAF 帧批处理数据更新 +- **仿真执行**:对接 chemical-chaos 微服务,支持任务创建、启停、暂停/恢复 +- **导入导出**:画布/项目的导入导出,Zod schema 校验数据完整性 +- **跨平台**:Web 浏览器 / Electron 桌面 / Tauri 轻量桌面三端统一 + +--- + +## 技术栈 + +| 类别 | 技术 | +|------|------| +| **框架** | Vue 3 (Composition API + ` + + diff --git a/apps/electron/src/renderer/src/env.d.ts b/apps/electron/src/renderer/src/env.d.ts new file mode 100644 index 0000000..f1e142e --- /dev/null +++ b/apps/electron/src/renderer/src/env.d.ts @@ -0,0 +1,17 @@ +/// +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + + const component: DefineComponent + export default component +} + +interface ImportMetaEnv { + readonly VITE_API_BASE_URL: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/apps/electron/src/renderer/src/main.ts b/apps/electron/src/renderer/src/main.ts new file mode 100644 index 0000000..8e1aa92 --- /dev/null +++ b/apps/electron/src/renderer/src/main.ts @@ -0,0 +1,5 @@ +import { bridge } from '@cslab-dcs/bridge' +import { createDCSApp } from '@cslab-dcs/core' + +const app = createDCSApp(bridge) +app.mount('#app') diff --git a/apps/electron/tsconfig.json b/apps/electron/tsconfig.json new file mode 100644 index 0000000..8918ee5 --- /dev/null +++ b/apps/electron/tsconfig.json @@ -0,0 +1,8 @@ +{ + "references": [ + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.preload.json" }, + { "path": "./tsconfig.web.json" } + ], + "files": [] +} diff --git a/apps/electron/tsconfig.node.json b/apps/electron/tsconfig.node.json new file mode 100644 index 0000000..be5e0a4 --- /dev/null +++ b/apps/electron/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "types": ["electron-vite/node"], + "outDir": "out" + }, + "include": ["src/main/**/*"], + "exclude": ["node_modules", "out", "dist"] +} diff --git a/apps/electron/tsconfig.preload.json b/apps/electron/tsconfig.preload.json new file mode 100644 index 0000000..0962b50 --- /dev/null +++ b/apps/electron/tsconfig.preload.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "types": ["electron-vite/node"], + "outDir": "out" + }, + "include": ["src/preload/**/*"], + "exclude": ["node_modules", "out", "dist"] +} diff --git a/apps/electron/tsconfig.web.json b/apps/electron/tsconfig.web.json new file mode 100644 index 0000000..1d3cc5a --- /dev/null +++ b/apps/electron/tsconfig.web.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "baseUrl": ".", + "paths": { + "@/*": ["../../packages/core/src/*"], + "@cslab-dcs/core": ["../../packages/core/src"], + "@cslab-dcs/bridge": ["../../packages/bridge/src"], + "@cslab-dcs/schema": ["../../packages/schema/src"] + }, + "types": ["element-plus/global"] + }, + "include": [ + "src/renderer/**/*", + "../../packages/core/src/**/*", + "../../packages/bridge/src/**/*", + "../../packages/request/src/**/*", + "../../packages/schema/src/**/*", + "../../packages/utils/src/**/*" + ], + "exclude": ["node_modules", "out", "dist"] +} diff --git a/apps/tauri/.eslintrc-auto-import.json b/apps/tauri/.eslintrc-auto-import.json new file mode 100644 index 0000000..87c4544 --- /dev/null +++ b/apps/tauri/.eslintrc-auto-import.json @@ -0,0 +1,3 @@ +{ + "globals": {} +} diff --git a/apps/tauri/index.html b/apps/tauri/index.html new file mode 100644 index 0000000..7fd8ddc --- /dev/null +++ b/apps/tauri/index.html @@ -0,0 +1,13 @@ + + + + + + + DCS Editor (Tauri) + + +
+ + + diff --git a/apps/tauri/package.json b/apps/tauri/package.json new file mode 100644 index 0000000..2895347 --- /dev/null +++ b/apps/tauri/package.json @@ -0,0 +1,30 @@ +{ + "name": "@cslab-dcs/tauri", + "type": "module", + "version": "1.0.0", + "private": true, + "scripts": { + "dev:web": "vite dev", + "dev": "tauri dev", + "build": "vue-tsc --noEmit && vite build", + "preview": "vite preview", + "tauri": "tauri" + }, + "dependencies": { + "@cslab-dcs/bridge": "workspace:*", + "@cslab-dcs/core": "workspace:*", + "@tauri-apps/api": "2.10.1", + "@tauri-apps/plugin-dialog": "^2.2.0", + "@tauri-apps/plugin-fs": "^2.2.0", + "@tauri-apps/plugin-opener": "^2.2.5", + "vue": "^3.5.13" + }, + "devDependencies": { + "@tauri-apps/cli": "2.10.0", + "@vitejs/plugin-vue": "^6.0.1", + "@vitejs/plugin-vue-jsx": "^4.1.1", + "typescript": "^5.9.3", + "vite": "^6.0.3", + "vue-tsc": "^2.2.0" + } +} diff --git a/apps/tauri/src-tauri/Cargo.lock b/apps/tauri/src-tauri/Cargo.lock new file mode 100644 index 0000000..c1aa7dd --- /dev/null +++ b/apps/tauri/src-tauri/Cargo.lock @@ -0,0 +1,5295 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.10.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.11+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cslab-dcs-tauri" +version = "1.0.0" +dependencies = [ + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-fs", + "tauri-plugin-opener", + "tauri-plugin-os", +] + +[[package]] +name = "cssparser" +version = "0.29.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "matches", + "phf 0.10.1", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.114", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.114", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.114", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.114", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.10.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.9.11+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link 0.2.1", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.10.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever", + "match_token", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.10.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "kuchikiki" +version = "0.8.8-speedreader" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" +dependencies = [ + "cssparser", + "html5ever", + "indexmap 2.13.0", + "selectors", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags 2.10.0", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen 0.11.3", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png", + "serde", + "thiserror 2.0.18", + "windows-sys 0.60.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.10.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.10.0", + "block2", + "libc", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.10.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.10.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.10.0", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-javascript-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" +dependencies = [ + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.10.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.10.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-javascript-core", + "objc2-security", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "open" +version = "5.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "os_info" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224" +dependencies = [ + "android_system_properties", + "log", + "nix", + "objc2", + "objc2-foundation", + "objc2-ui-kit", + "serde", + "windows-sys 0.61.2", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared 0.8.0", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64 0.22.1", + "indexmap 2.13.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.10+spec-1.0.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "reqwest" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.114", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" +dependencies = [ + "bitflags 1.3.2", + "cssparser", + "derive_more", + "fxhash", + "log", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.13.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "servo_arc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.34.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" +dependencies = [ + "bitflags 2.10.0", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dispatch", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "parking_lot", + "raw-window-handle", + "scopeguard", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1025aa560c1eaa14ef96de5540cbe13ed6be1933ebe4398338c96bc370b807c6" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76809f63061c8b25537b87f46b8733b31397cf419706dc874e1602be6479ba90" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "toml 0.9.11+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2ebe49d690ccaea93aa81fff99277d4f445968f085ba13be67859151e9e4b8" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.114", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1119f651b0187c686c0fc72c66bba311e560e1b5f61869086ce788d43be6cf41" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.114", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692a77abd8b8773e107a42ec0e05b767b8d2b7ece76ab36c6c3947e34df9f53f" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "toml 0.9.11+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9204b425d9be8d12aa60c2a83a289cf7d1caae40f57f336ed1155b3a5c0e359b" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed390cc669f937afeb8b28032ce837bac8ea023d975a2e207375ec05afaf1804" +dependencies = [ + "anyhow", + "dunce", + "glob", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 0.9.11+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc624469b06f59f5a29f874bbc61a2ed737c0f9c23ef09855a292c389c42e83f" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "url", + "windows", + "zbus", +] + +[[package]] +name = "tauri-plugin-os" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f08346c8deb39e96f86973da0e2d76cbb933d7ac9b750f6dc4daf955a6f997" +dependencies = [ + "gethostname", + "log", + "os_info", + "serde", + "serde_json", + "serialize-to-javascript", + "sys-locale", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + +[[package]] +name = "tauri-runtime" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b885ffeac82b00f1f6fd292b6e5aabfa7435d537cef57d11e38a489956535651" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5204682391625e867d16584fedc83fc292fb998814c9f7918605c789cd876314" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcd169fccdff05eff2c1033210b9b94acd07a47e6fa9a3431cf09cfd4f01c87e" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dunce", + "glob", + "html5ever", + "http", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.3", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 0.9.11+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" +dependencies = [ + "dunce", + "embed-resource", + "toml 0.9.11+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "time" +version = "0.3.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +dependencies = [ + "indexmap 2.13.0", + "serde_core", + "serde_spanned 1.0.4", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.14", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.13.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "winnow 0.7.14", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow 0.7.14", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png", + "serde", + "thiserror 2.0.18", + "windows-sys 0.60.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.114", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "wry" +version = "0.54.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ed1a195b0375491dd15a7066a10251be217ce743cf4bbbbdcf5391d6473bee0" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dpi", + "dunce", + "gdkx11", + "gtk", + "html5ever", + "http", + "javascriptcore-rs", + "jni", + "kuchikiki", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfeff997a0aaa3eb20c4652baf788d2dfa6d2839a0ead0b3ff69ce2f9c4bdd1" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.14", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bbd5a90dbe8feee5b13def448427ae314ccd26a49cac47905cafefb9ff846f1" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.114", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +dependencies = [ + "serde", + "winnow 0.7.14", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57cf3aa6855b23711ee9852dfc97dfaa51c45feaba5b645d0c777414d494a961" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a616990af1a287837c4fe6596ad77ef57948f787e46ce28e166facc0cc1cb75" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zmij" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" + +[[package]] +name = "zvariant" +version = "5.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b64ef4f40c7951337ddc7023dd03528a57a3ce3408ee9da5e948bd29b232c4" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 0.7.14", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "484d5d975eb7afb52cc6b929c13d3719a20ad650fea4120e6310de3fc55e415c" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.114", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.114", + "winnow 0.7.14", +] diff --git a/apps/tauri/src-tauri/Cargo.toml b/apps/tauri/src-tauri/Cargo.toml new file mode 100644 index 0000000..46d9950 --- /dev/null +++ b/apps/tauri/src-tauri/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "cslab-dcs-editor" +version = "1.0.0" +description = "DCS Editor - CSLAB DCS 编辑器" +authors = [ "cslab" ] +edition = "2021" + +[lib] +name = "cslab_dcs_tauri_lib" +crate-type = [ + "staticlib", + "cdylib", + "rlib" +] + +[build-dependencies] +tauri-build = { version = "2.5.4", features = [] } + +[dependencies] +tauri = { version = "2.10.0", features = [] } +tauri-plugin-opener = "2.2.5" +tauri-plugin-dialog = "2.2.0" +tauri-plugin-fs = "2.2.0" +tauri-plugin-os = "2" +serde = { version = "1", features = [ "derive" ] } +serde_json = "1" + +[features] +# this feature is used for production builds or when `devPath` points to the filesystem +# DO NOT REMOVE!! +custom-protocol = [ "tauri/custom-protocol" ] diff --git a/apps/tauri/src-tauri/build.rs b/apps/tauri/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/apps/tauri/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/apps/tauri/src-tauri/capabilities/default.json b/apps/tauri/src-tauri/capabilities/default.json new file mode 100644 index 0000000..b043360 --- /dev/null +++ b/apps/tauri/src-tauri/capabilities/default.json @@ -0,0 +1,13 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Capability for the main window", + "windows": ["main"], + "permissions": [ + "core:default", + "dialog:default", + "opener:default", + "fs:default", + "os:default" + ] +} diff --git a/apps/tauri/src-tauri/icons/128x128.png b/apps/tauri/src-tauri/icons/128x128.png new file mode 100644 index 0000000..216e00e Binary files /dev/null and b/apps/tauri/src-tauri/icons/128x128.png differ diff --git a/apps/tauri/src-tauri/icons/128x128@2x.png b/apps/tauri/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000..8b18773 Binary files /dev/null and b/apps/tauri/src-tauri/icons/128x128@2x.png differ diff --git a/apps/tauri/src-tauri/icons/32x32.png b/apps/tauri/src-tauri/icons/32x32.png new file mode 100644 index 0000000..52e3a5b Binary files /dev/null and b/apps/tauri/src-tauri/icons/32x32.png differ diff --git a/apps/tauri/src-tauri/icons/Square107x107Logo.png b/apps/tauri/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000..bbc862c Binary files /dev/null and b/apps/tauri/src-tauri/icons/Square107x107Logo.png differ diff --git a/apps/tauri/src-tauri/icons/Square142x142Logo.png b/apps/tauri/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000..9b9e36e Binary files /dev/null and b/apps/tauri/src-tauri/icons/Square142x142Logo.png differ diff --git a/apps/tauri/src-tauri/icons/Square150x150Logo.png b/apps/tauri/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 0000000..9813914 Binary files /dev/null and b/apps/tauri/src-tauri/icons/Square150x150Logo.png differ diff --git a/apps/tauri/src-tauri/icons/Square284x284Logo.png b/apps/tauri/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000..e6e67fd Binary files /dev/null and b/apps/tauri/src-tauri/icons/Square284x284Logo.png differ diff --git a/apps/tauri/src-tauri/icons/Square30x30Logo.png b/apps/tauri/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000..0e073cb Binary files /dev/null and b/apps/tauri/src-tauri/icons/Square30x30Logo.png differ diff --git a/apps/tauri/src-tauri/icons/Square310x310Logo.png b/apps/tauri/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000..f79a3fd Binary files /dev/null and b/apps/tauri/src-tauri/icons/Square310x310Logo.png differ diff --git a/apps/tauri/src-tauri/icons/Square44x44Logo.png b/apps/tauri/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000..80bbfbe Binary files /dev/null and b/apps/tauri/src-tauri/icons/Square44x44Logo.png differ diff --git a/apps/tauri/src-tauri/icons/Square71x71Logo.png b/apps/tauri/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000..eff032a Binary files /dev/null and b/apps/tauri/src-tauri/icons/Square71x71Logo.png differ diff --git a/apps/tauri/src-tauri/icons/Square89x89Logo.png b/apps/tauri/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 0000000..ac1f64a Binary files /dev/null and b/apps/tauri/src-tauri/icons/Square89x89Logo.png differ diff --git a/apps/tauri/src-tauri/icons/StoreLogo.png b/apps/tauri/src-tauri/icons/StoreLogo.png new file mode 100644 index 0000000..7a8847d Binary files /dev/null and b/apps/tauri/src-tauri/icons/StoreLogo.png differ diff --git a/apps/tauri/src-tauri/icons/icon.icns b/apps/tauri/src-tauri/icons/icon.icns new file mode 100644 index 0000000..349863d Binary files /dev/null and b/apps/tauri/src-tauri/icons/icon.icns differ diff --git a/apps/tauri/src-tauri/icons/icon.ico b/apps/tauri/src-tauri/icons/icon.ico new file mode 100644 index 0000000..b054f5e Binary files /dev/null and b/apps/tauri/src-tauri/icons/icon.ico differ diff --git a/apps/tauri/src-tauri/icons/icon.png b/apps/tauri/src-tauri/icons/icon.png new file mode 100755 index 0000000..82c16d8 Binary files /dev/null and b/apps/tauri/src-tauri/icons/icon.png differ diff --git a/apps/tauri/src-tauri/icons/icon_512.png b/apps/tauri/src-tauri/icons/icon_512.png new file mode 100644 index 0000000..98ea882 Binary files /dev/null and b/apps/tauri/src-tauri/icons/icon_512.png differ diff --git a/apps/tauri/src-tauri/src/lib.rs b/apps/tauri/src-tauri/src/lib.rs new file mode 100644 index 0000000..6d71690 --- /dev/null +++ b/apps/tauri/src-tauri/src/lib.rs @@ -0,0 +1,10 @@ +// #[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_fs::init()) + .plugin(tauri_plugin_os::init()) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/apps/tauri/src-tauri/src/main.rs b/apps/tauri/src-tauri/src/main.rs new file mode 100644 index 0000000..98e1040 --- /dev/null +++ b/apps/tauri/src-tauri/src/main.rs @@ -0,0 +1,6 @@ +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + cslab_dcs_tauri_lib::run() +} diff --git a/apps/tauri/src-tauri/tauri.conf.json b/apps/tauri/src-tauri/tauri.conf.json new file mode 100644 index 0000000..48eb7b9 --- /dev/null +++ b/apps/tauri/src-tauri/tauri.conf.json @@ -0,0 +1,40 @@ +{ + "identifier": "com.cslab.dcs.editor", + "build": { + "beforeDevCommand": "pnpm dev:web", + "beforeBuildCommand": "pnpm build:web", + "devUrl": "http://localhost:1420", + "frontendDist": "../dist" + }, + "bundle": { + "active": true, + "targets": "all", + + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + }, + "app": { + "withGlobalTauri": false, + "windows": [ + { + "title": "DCS Editor", + "width": 800, + "height": 600, + "dragDropEnabled": false + } + ], + "security": { + "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' https:; font-src 'self' data:" + } + }, + "plugins": { + + "fs": {} + + } +} diff --git a/apps/tauri/src/main.ts b/apps/tauri/src/main.ts new file mode 100644 index 0000000..6f20faf --- /dev/null +++ b/apps/tauri/src/main.ts @@ -0,0 +1,9 @@ +import { WebBridge } from '@cslab-dcs/bridge' +import { TauriBridge } from '@cslab-dcs/bridge/tauri' +import { createDCSApp } from '@cslab-dcs/core' + +const isTauri = !!(window as any).__TAURI_INTERNALS__ || !!(window as any).__TAURI__ +const bridge = isTauri ? new TauriBridge() : new WebBridge() + +const app = createDCSApp(bridge) +app.mount('#app') diff --git a/apps/tauri/tsconfig.json b/apps/tauri/tsconfig.json new file mode 100644 index 0000000..1c6a70b --- /dev/null +++ b/apps/tauri/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["../../packages/core/src/*"], + "@cslab-dcs/core": ["../../packages/core/src"], + "@cslab-dcs/bridge": ["../../packages/bridge/src"], + "@cslab-dcs/schema": ["../../packages/schema/src"] + }, + "types": ["element-plus/global"] + }, + "include": [ + "src/**/*", + "../../packages/core/src/**/*", + "../../packages/core/env.d.ts", + "../../packages/bridge/src/**/*", + "../../packages/request/src/**/*", + "../../packages/schema/src/**/*", + "../../packages/utils/src/**/*" + ], + "exclude": ["node_modules", "dist", "src-tauri"] +} diff --git a/apps/tauri/vite.config.ts b/apps/tauri/vite.config.ts new file mode 100644 index 0000000..f59e706 --- /dev/null +++ b/apps/tauri/vite.config.ts @@ -0,0 +1,36 @@ +import type { UserConfig } from 'vite' +import { resolve } from 'node:path' +import process from 'node:process' +import { defineConfig } from 'vite' +import { createSharedViteConfig } from '../../packages/core/vite.shared' + +const coreRoot = resolve(__dirname, '../../packages/core') + +const host = process.env.VITE_DEV_SERVER_HOST + +// 使用类型断言绕过 pnpm 导致的 Vite 依赖重复安装类型不兼容问题 +export default defineConfig({ + ...createSharedViteConfig(coreRoot) as UserConfig, + root: __dirname, + clearScreen: false, + server: { + port: 1420, + strictPort: true, + host: host || false, + hmr: host + ? { + protocol: 'ws', + host, + port: 1421, + } + : undefined, + watch: { + // 3. tell vite to ignore watching `src-tauri` + ignored: ['**/src-tauri/**'], + }, + }, + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}) diff --git a/apps/web/.env b/apps/web/.env new file mode 100644 index 0000000..61a5b1f --- /dev/null +++ b/apps/web/.env @@ -0,0 +1,3 @@ +# 默认环境变量配置(会被 .env.development 和 .env.production 覆盖) +# VITE_API_BASE_URL=https://cslab.oberyun.com +VITE_API_BASE_URL=http://192.168.1.110:8001 diff --git a/apps/web/.env.development b/apps/web/.env.development new file mode 100644 index 0000000..e4a0c06 --- /dev/null +++ b/apps/web/.env.development @@ -0,0 +1,2 @@ +# 开发环境配置 +VITE_API_BASE_URL=http://192.168.1.110:8001 diff --git a/apps/web/.env.production b/apps/web/.env.production new file mode 100644 index 0000000..31fb191 --- /dev/null +++ b/apps/web/.env.production @@ -0,0 +1,2 @@ +# 生产环境配置 +VITE_API_BASE_URL=https://cslab.oberyun.com diff --git a/apps/web/.eslintrc-auto-import.json b/apps/web/.eslintrc-auto-import.json new file mode 100644 index 0000000..87c4544 --- /dev/null +++ b/apps/web/.eslintrc-auto-import.json @@ -0,0 +1,3 @@ +{ + "globals": {} +} diff --git a/apps/web/index.html b/apps/web/index.html new file mode 100644 index 0000000..d624da0 --- /dev/null +++ b/apps/web/index.html @@ -0,0 +1,13 @@ + + + + + + + DCS Editor (Web) + + +
+ + + diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..5b3ed03 --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,24 @@ +{ + "name": "@cslab-dcs/web", + "type": "module", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "vite --host", + "build": "vue-tsc --noEmit && vite build", + "preview": "vite preview", + "typecheck": "vue-tsc --noEmit" + }, + "dependencies": { + "@cslab-dcs/bridge": "workspace:*", + "@cslab-dcs/core": "workspace:*", + "vue": "^3.5.13" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.1", + "@vitejs/plugin-vue-jsx": "^4.1.1", + "typescript": "^5.9.3", + "vite": "^6.0.3", + "vue-tsc": "^2.2.0" + } +} diff --git a/apps/web/src/env.d.ts b/apps/web/src/env.d.ts new file mode 100644 index 0000000..b4345d9 --- /dev/null +++ b/apps/web/src/env.d.ts @@ -0,0 +1,16 @@ +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + + const component: DefineComponent + export default component +} + +interface ImportMetaEnv { + readonly VITE_API_BASE_URL: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/apps/web/src/main.ts b/apps/web/src/main.ts new file mode 100644 index 0000000..8e1aa92 --- /dev/null +++ b/apps/web/src/main.ts @@ -0,0 +1,5 @@ +import { bridge } from '@cslab-dcs/bridge' +import { createDCSApp } from '@cslab-dcs/core' + +const app = createDCSApp(bridge) +app.mount('#app') diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 0000000..2a1a471 --- /dev/null +++ b/apps/web/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["../../packages/core/src/*"], + "@cslab-dcs/core": ["../../packages/core/src"], + "@cslab-dcs/bridge": ["../../packages/bridge/src"], + "@cslab-dcs/schema": ["../../packages/schema/src"] + }, + "types": ["element-plus/global"] + }, + "include": [ + "src/**/*", + "../../packages/core/src/**/*", + "../../packages/core/src/auto-imports.d.ts", + "../../packages/core/env.d.ts", + "../../packages/bridge/src/**/*", + "../../packages/request/src/**/*", + "../../packages/schema/src/**/*", + "../../packages/utils/src/**/*" + ], + "exclude": ["node_modules", "dist"] +} diff --git a/apps/web/unocss.config.ts b/apps/web/unocss.config.ts new file mode 100644 index 0000000..db42b18 --- /dev/null +++ b/apps/web/unocss.config.ts @@ -0,0 +1,31 @@ +import { + defineConfig, + presetAttributify, + presetIcons, + presetTypography, + presetUno, + transformerDirectives, + transformerVariantGroup, +} from 'unocss' +import presetChinese from 'unocss-preset-chinese' +import presetEase from 'unocss-preset-ease' + +export default defineConfig({ + presets: [ + presetUno(), + presetAttributify(), + presetChinese(), + presetEase(), + presetTypography(), + presetIcons({ + scale: 1.2, + warn: true, + }), + ], + shortcuts: [ + ['flex-center', 'flex items-center justify-center'], + ['flex-between', 'flex items-center justify-between'], + ['flex-end', 'flex items-end justify-between'], + ], + transformers: [transformerDirectives(), transformerVariantGroup()], +}) diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts new file mode 100644 index 0000000..c7563d7 --- /dev/null +++ b/apps/web/vite.config.ts @@ -0,0 +1,25 @@ +import type { UserConfig } from 'vite' +import { resolve } from 'node:path' +import { defineConfig, loadEnv, mergeConfig } from 'vite' +import { createSharedViteConfig } from '../../packages/core/vite.shared' + +// Core 包的根目录 +const coreRoot = resolve(__dirname, '../../packages/core') + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, coreRoot, 'VITE_') + + const sharedConfig = createSharedViteConfig(coreRoot, env) as UserConfig + + return mergeConfig(sharedConfig, { + root: __dirname, + base: env.VITE_BASE_URL, // 确保相对路径,方便部署 + build: { + outDir: 'dist', + emptyOutDir: true, + }, + server: { + port: 5173, + }, + }) +}) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..a7b55fb --- /dev/null +++ b/docs/README.md @@ -0,0 +1,3 @@ +# README + +本目录下记录与AI的prompt和AI出的计划和实际操作等等。 diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..2681550 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,8 @@ +import antfu from '@antfu/eslint-config' + +export default antfu({ + rules: { + 'no-console': 'off', + }, + ignores: ['**/skills/**', 'research.md'], +}) diff --git a/package.json b/package.json new file mode 100644 index 0000000..42e48ad --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "cslab-dcs", + "type": "module", + "version": "1.0.0", + "private": true, + "packageManager": "pnpm@9.15.0", + "description": "DCS Editor - A cross-platform configuration editor", + "engines": { + "node": ">=20.0.0", + "pnpm": ">=9.0.0" + }, + "scripts": { + "dev:web": "pnpm -F @cslab-dcs/web dev", + "dev:electron": "pnpm -F @cslab-dcs/electron dev", + "dev:tauri": "pnpm -F @cslab-dcs/tauri tauri dev", + "build:web": "pnpm -F @cslab-dcs/web build", + "build:electron:win": "pnpm -F @cslab-dcs/electron build:win", + "build:tauri": "pnpm -F @cslab-dcs/tauri tauri build", + "format": "eslint . --fix", + "typecheck": "pnpm -r typecheck", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "clean:all": "pnpm -r exec rm -rf dist out node_modules && rm -rf node_modules" + }, + "devDependencies": { + "@antfu/eslint-config": "^7.2.0", + "@playwright/test": "^1.58.2", + "@types/node": "^22.19.7", + "@vitest/coverage-v8": "^4.1.0", + "@vue/test-utils": "^2.4.6", + "eslint": "^9.39.2", + "happy-dom": "^20.8.4", + "typescript": "^5.9.3", + "vitest": "^4.1.0" + } +} diff --git a/packages/bridge/package.json b/packages/bridge/package.json new file mode 100644 index 0000000..7d9d089 --- /dev/null +++ b/packages/bridge/package.json @@ -0,0 +1,32 @@ +{ + "name": "@cslab-dcs/bridge", + "type": "module", + "version": "1.0.0", + "private": true, + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts" + }, + "./tauri": { + "types": "./src/tauri.ts", + "import": "./src/tauri.ts" + } + }, + "main": "./src/index.ts", + "types": "./src/index.ts", + "scripts": { + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@tauri-apps/api": "2.10.1", + "@tauri-apps/plugin-dialog": "^2.2.0", + "@tauri-apps/plugin-fs": "^2.2.0", + "@tauri-apps/plugin-opener": "^2.2.5", + "@tauri-apps/plugin-os": "^2.3.2" + }, + "devDependencies": { + "electron": "^33.2.1", + "typescript": "^5.9.3" + } +} diff --git a/packages/bridge/src/adapters/electron.ts b/packages/bridge/src/adapters/electron.ts new file mode 100644 index 0000000..01a4be8 --- /dev/null +++ b/packages/bridge/src/adapters/electron.ts @@ -0,0 +1,70 @@ +import type { + ConfirmOptions, + FileDialogOptions, + IDialogService, + IFileService, + IPlatformBridge, + ISystemService, + MessageOptions, + PlatformType, + SaveDialogOptions, +} from '../types' + +// Electron IPC 接口定义(需要在 electron preload 中实现) +interface IElectronAPI { + ipcRenderer: { + invoke: (channel: string, ...args: unknown[]) => Promise + send: (channel: string, ...args: unknown[]) => void + on: (channel: string, listener: (event: unknown, ...args: unknown[]) => void) => void + removeListener: (channel: string, listener: (...args: unknown[]) => void) => void + } +} + +declare global { + interface Window { + electron: IElectronAPI + } +} + +export class ElectronBridge implements IPlatformBridge { + readonly platform: PlatformType = 'electron' + + file: IFileService = { + async read(path: string): Promise { + return (await window.electron.ipcRenderer.invoke('file:read', path)) as string + }, + async write(path: string, content: string): Promise { + await window.electron.ipcRenderer.invoke('file:write', path, content) + }, + async exists(path: string): Promise { + return (await window.electron.ipcRenderer.invoke('file:exists', path)) as boolean + }, + async openDialog(options?: FileDialogOptions): Promise { + return (await window.electron.ipcRenderer.invoke('dialog:open', options)) as string | string[] | null + }, + async saveDialog(options?: SaveDialogOptions): Promise { + return (await window.electron.ipcRenderer.invoke('dialog:save', options)) as string | null + }, + } + + dialog: IDialogService = { + async message(options: MessageOptions): Promise { + await window.electron.ipcRenderer.invoke('dialog:message', options) + }, + async confirm(options: ConfirmOptions): Promise { + return (await window.electron.ipcRenderer.invoke('dialog:confirm', options)) as boolean + }, + } + + system: ISystemService = { + async getAppVersion(): Promise { + return (await window.electron.ipcRenderer.invoke('app:version')) as string + }, + async getPlatformInfo() { + return (await window.electron.ipcRenderer.invoke('app:info')) as any + }, + async openExternal(url: string): Promise { + await window.electron.ipcRenderer.invoke('shell:open', url) + }, + } +} diff --git a/packages/bridge/src/adapters/tauri.ts b/packages/bridge/src/adapters/tauri.ts new file mode 100644 index 0000000..02e1cc0 --- /dev/null +++ b/packages/bridge/src/adapters/tauri.ts @@ -0,0 +1,86 @@ +import type { + ConfirmOptions, + FileDialogOptions, + IDialogService, + IFileService, + IPlatformBridge, + ISystemService, + MessageOptions, + PlatformType, + SaveDialogOptions, +} from '../types' +import { getTauriVersion, getVersion } from '@tauri-apps/api/app' +import { ask, message, open, save } from '@tauri-apps/plugin-dialog' +import { exists, readTextFile, writeTextFile } from '@tauri-apps/plugin-fs' +import { openUrl } from '@tauri-apps/plugin-opener' +import { arch, version as osVersion, platform } from '@tauri-apps/plugin-os' + +export class TauriBridge implements IPlatformBridge { + readonly platform: PlatformType = 'tauri' + + file: IFileService = { + async read(path: string): Promise { + return await readTextFile(path) + }, + async write(path: string, content: string): Promise { + await writeTextFile(path, content) + }, + async exists(path: string): Promise { + return await exists(path) + }, + async openDialog(options?: FileDialogOptions): Promise { + const selected = await open({ + multiple: options?.multiple ?? false, + directory: options?.directory ?? false, + filters: options?.filters, + defaultPath: options?.defaultPath, + }) + + if (selected === null) + return null + // 这里的类型转换取决于 multiple 选项 + return selected as string | string[] + }, + async saveDialog(options?: SaveDialogOptions): Promise { + return await save({ + defaultPath: options?.defaultPath, + filters: options?.filters, + }) + }, + } + + dialog: IDialogService = { + async message(options: MessageOptions): Promise { + await message(options.message, { + title: options.title, + kind: options.type === 'error' ? 'error' : options.type === 'warning' ? 'warning' : 'info', + }) + }, + async confirm(options: ConfirmOptions): Promise { + return await ask(options.message, { + title: options.title, + kind: options.type === 'error' ? 'error' : options.type === 'warning' ? 'warning' : 'info', + okLabel: options.okLabel || 'Yes', + cancelLabel: options.cancelLabel || 'No', + }) + }, + } + + system: ISystemService = { + async getAppVersion(): Promise { + return await getVersion() + }, + async getPlatformInfo() { + return { + name: 'tauri', + version: await getTauriVersion(), + os: await platform(), + osVersion: await osVersion(), + arch: await arch(), + } + }, + async openExternal(url: string): Promise { + await openUrl(url) + }, + } +} diff --git a/packages/bridge/src/adapters/web.ts b/packages/bridge/src/adapters/web.ts new file mode 100644 index 0000000..088e1bd --- /dev/null +++ b/packages/bridge/src/adapters/web.ts @@ -0,0 +1,96 @@ +import type { + ConfirmOptions, + FileDialogOptions, + IDialogService, + IFileService, + IPlatformBridge, + ISystemService, + MessageOptions, + PlatformType, + SaveDialogOptions, +} from '../types' + +const MAC_OS_VERSION_RE = /Mac OS X ([\d_]+)/ +const WINDOWS_VERSION_RE = /Windows NT ([\d.]+)/ +const UNDERSCORE_RE = /_/g + +function parseOsVersion(userAgent: string): string | undefined { + const macMatch = userAgent.match(MAC_OS_VERSION_RE) + if (macMatch) + return macMatch[1].replace(UNDERSCORE_RE, '.') + + const winMatch = userAgent.match(WINDOWS_VERSION_RE) + if (winMatch) + return winMatch[1] + + return undefined +} + +export class WebBridge implements IPlatformBridge { + readonly platform: PlatformType = 'web' + + file: IFileService = { + async read(_path: string): Promise { + console.warn('Web environment does not support file system access directly.') + return '' + }, + async write(_path: string, _content: string): Promise { + console.warn('Web environment does not support file system access directly.') + }, + async exists(_path: string): Promise { + return false + }, + async openDialog(_options?: FileDialogOptions): Promise { + return new Promise((resolve) => { + const input = document.createElement('input') + input.type = 'file' + // Web端模拟,仅用于演示 + input.onchange = (e) => { + const files = (e.target as HTMLInputElement).files + if (files && files.length > 0) { + // Web端通常不能获取完整路径,这里只是模拟 + resolve(files[0].name) + } + else { + resolve(null) + } + } + input.click() + }) + }, + async saveDialog(_options?: SaveDialogOptions): Promise { + console.warn('Web environment save dialog not fully supported.') + return null + }, + } + + dialog: IDialogService = { + async message(options: MessageOptions): Promise { + // eslint-disable-next-line no-alert + alert(`${options.title ? `${options.title}\n` : ''}${options.message}`) + }, + async confirm(options: ConfirmOptions): Promise { + // eslint-disable-next-line no-alert + return confirm(`${options.title ? `${options.title}\n` : ''}${options.message}`) + }, + } + + system: ISystemService = { + async getAppVersion(): Promise { + return '1.0.0' + }, + async getPlatformInfo() { + const osVersion = parseOsVersion(navigator.userAgent) + return { + name: 'web', + version: navigator.userAgent, + os: navigator.platform, + osVersion, + arch: 'unknown', + } + }, + async openExternal(url: string): Promise { + window.open(url, '_blank') + }, + } +} diff --git a/packages/bridge/src/index.ts b/packages/bridge/src/index.ts new file mode 100644 index 0000000..fea7a2d --- /dev/null +++ b/packages/bridge/src/index.ts @@ -0,0 +1,24 @@ +import type { IPlatformBridge } from './types' +import { ElectronBridge } from './adapters/electron' +import { WebBridge } from './adapters/web' + +export { ElectronBridge } from './adapters/electron' +export { WebBridge } from './adapters/web' +export * from './types' + +export function createBridge(): IPlatformBridge { + if (typeof window !== 'undefined') { + const win = window as any + if (win.electron && win.electron.ipcRenderer) { + return new ElectronBridge() + } + + // Tauri 检测逻辑 - 注意:这里不能引用 TauriBridge,否则会破坏 Electron 构建 + // Tauri App 必须手动初始化 Bridge 并通过 provide 注入,或者我们依赖 userAgent 判断 + // 但 instantiation 必须在 app 层做。 + } + + return new WebBridge() +} + +export const bridge = createBridge() diff --git a/packages/bridge/src/tauri.ts b/packages/bridge/src/tauri.ts new file mode 100644 index 0000000..4e39481 --- /dev/null +++ b/packages/bridge/src/tauri.ts @@ -0,0 +1 @@ +export * from './adapters/tauri' diff --git a/packages/bridge/src/types.ts b/packages/bridge/src/types.ts new file mode 100644 index 0000000..1bc0220 --- /dev/null +++ b/packages/bridge/src/types.ts @@ -0,0 +1,70 @@ +/** + * 平台桥接类型定义 + */ + +export type PlatformType = 'web' | 'electron' | 'tauri' + +export interface FileDialogOptions { + title?: string + defaultPath?: string + filters?: { + name: string + extensions: string[] + }[] + multiple?: boolean + directory?: boolean +} + +export interface SaveDialogOptions { + title?: string + defaultPath?: string + filters?: { + name: string + extensions: string[] + }[] +} + +export interface MessageOptions { + title?: string + message: string + type?: 'info' | 'warning' | 'error' +} + +export interface ConfirmOptions extends MessageOptions { + okLabel?: string + cancelLabel?: string +} + +export interface PlatformInfo { + name: PlatformType + version: string + os: string + osVersion?: string + arch: string +} + +export interface IFileService { + read: (path: string) => Promise + write: (path: string, content: string) => Promise + exists: (path: string) => Promise + openDialog: (options?: FileDialogOptions) => Promise + saveDialog: (options?: SaveDialogOptions) => Promise +} + +export interface IDialogService { + message: (options: MessageOptions) => Promise + confirm: (options: ConfirmOptions) => Promise +} + +export interface ISystemService { + getAppVersion: () => Promise + getPlatformInfo: () => Promise + openExternal: (url: string) => Promise +} + +export interface IPlatformBridge { + readonly platform: PlatformType + file: IFileService + dialog: IDialogService + system: ISystemService +} diff --git a/packages/bridge/tsconfig.json b/packages/bridge/tsconfig.json new file mode 100644 index 0000000..af87b77 --- /dev/null +++ b/packages/bridge/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "rootDir": "./src", + "types": ["electron", "node"], + "outDir": "./dist" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/core/.env b/packages/core/.env new file mode 100644 index 0000000..ac348d6 --- /dev/null +++ b/packages/core/.env @@ -0,0 +1,4 @@ +# 默认环境变量配置(会被 .env.development 和 .env.production 覆盖) +VITE_API_BASE_URL=https://cslab.oberyun.com + +VITE_BASE_URL=/dcs-web/ \ No newline at end of file diff --git a/packages/core/.env.development b/packages/core/.env.development new file mode 100644 index 0000000..74b3d3a --- /dev/null +++ b/packages/core/.env.development @@ -0,0 +1,3 @@ +# 开发环境配置 +VITE_API_BASE_URL=http://192.168.1.110:8001 +VITE_WS_DOMAIN=ws://192.168.1.110:6600 diff --git a/packages/core/.env.production b/packages/core/.env.production new file mode 100644 index 0000000..31fb191 --- /dev/null +++ b/packages/core/.env.production @@ -0,0 +1,2 @@ +# 生产环境配置 +VITE_API_BASE_URL=https://cslab.oberyun.com diff --git a/packages/core/env.d.ts b/packages/core/env.d.ts new file mode 100644 index 0000000..15a4e20 --- /dev/null +++ b/packages/core/env.d.ts @@ -0,0 +1,22 @@ +/// +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + + const component: DefineComponent + export default component +} + +interface ImportMetaEnv { + readonly VITE_API_BASE_URL: string + // 更多环境变量可以在这里添加... +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} + +interface Window { + // 可以扩展 window 类型 +} diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..2b977c9 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,50 @@ +{ + "name": "@cslab-dcs/core", + "type": "module", + "version": "1.0.0", + "private": true, + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts" + }, + "./vite.shared": { + "types": "./vite.shared.d.ts", + "import": "./vite.shared.ts" + } + }, + "main": "./src/index.ts", + "types": "./src/index.ts", + "scripts": { + "typecheck": "vue-tsc --noEmit" + }, + "dependencies": { + "@cslab-dcs/bridge": "workspace:*", + "@cslab-dcs/schema": "workspace:*", + "@fortawesome/fontawesome-free": "^7.1.0", + "@vueuse/core": "^12.0.0", + "dayjs": "^1.11.19", + "element-plus": "^2.9.0", + "es-toolkit": "^1.44.0", + "mitt": "^3.0.1", + "ofetch": "^1.5.1", + "pinia": "^3.0.0", + "vue": "^3.5.13", + "vue-router": "^4.5.0", + "zod": "^3.24.1" + }, + "devDependencies": { + "@unocss/preset-attributify": "^66.6.0", + "@vitejs/plugin-vue": "^6.0.1", + "@vitejs/plugin-vue-jsx": "^4.1.1", + "sass": "^1.83.0", + "typescript": "^5.9.3", + "unocss": "^66.6.0", + "unocss-preset-chinese": "^0.3.3", + "unocss-preset-ease": "^0.0.4", + "unplugin-auto-import": "^19.0.0", + "unplugin-vue-components": "^28.0.0", + "vite": "^6.0.3", + "vue-tsc": "^2.2.0" + } +} diff --git a/packages/core/src/App.vue b/packages/core/src/App.vue new file mode 100644 index 0000000..343b6dc --- /dev/null +++ b/packages/core/src/App.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/packages/core/src/api/canvas.ts b/packages/core/src/api/canvas.ts new file mode 100644 index 0000000..9f3b06f --- /dev/null +++ b/packages/core/src/api/canvas.ts @@ -0,0 +1,127 @@ +import type { Layer } from '@cslab-dcs/schema' +import type { CanvasDetail, CanvasSummary } from '@/request' +import { requestClient } from '@/request' +import pinia, { useAppStore, useProjectStore } from '@/stores' + +// 画布服务层:对 UI 提供稳定接口,屏蔽 request 底层适配器 +export interface CanvasCreatePayload { + name: string + description?: string + thumbnail?: string + width?: number + height?: number +} + +export interface CanvasUpdatePayload { + id: string + name: string + description?: string +} + +export interface CanvasDuplicatePayload { + id: string + name?: string +} + +export interface SaveCanvasComponentsPayload { + id: string + components: Layer[] +} + +export interface UpdateCanvasBaseLayerPayload { + id: string + width: number + height: number + thumbnail?: string | null + lockAspectRatio?: boolean + backgroundColor?: string +} + +export type CanvasListItem = CanvasSummary +export type CanvasData = CanvasDetail + +function getRequestClient() { + const { isWeb } = useAppStore(pinia) + if (!isWeb) + return requestClient.local + + return requestClient.local +} + +export async function listCanvasesApi(): Promise { + const { projectId } = useProjectStore(pinia) + + if (!projectId) + return [] + + return getRequestClient().canvas.list(projectId) +} + +export async function getCanvasByIdApi(canvasId: string): Promise { + return getRequestClient().canvas.getById(canvasId) +} + +export async function createCanvasApi(payload: CanvasCreatePayload): Promise { + const { projectId } = useProjectStore(pinia) + if (!projectId) + throw new Error('Project ID is required to create a canvas') + + return getRequestClient().canvas.create({ + projectId, + name: payload.name, + description: payload.description, + thumbnail: payload.thumbnail, + width: payload.width, + height: payload.height, + }) +} + +export async function updateCanvasApi(payload: CanvasUpdatePayload): Promise { + console.log('payload', payload) + + return getRequestClient().canvas.update({ + canvasId: payload.id, + name: payload.name, + description: payload.description, + }) +} + +export async function duplicateCanvasApi(payload: CanvasDuplicatePayload): Promise { + return getRequestClient().canvas.duplicate({ + canvasId: payload.id, + name: payload.name, + }) +} + +export async function deleteCanvasApi(canvasId: string): Promise { + return getRequestClient().canvas.remove({ canvasId }) +} + +// 批量更新画布排序 +export async function reorderCanvasesApi(canvasIds: string[]): Promise { + const { projectId } = useProjectStore(pinia) + if (!projectId) + throw new Error('Project ID is required to reorder canvases') + + return getRequestClient().canvas.reorder({ projectId, canvasIds }) +} + +// 编辑器自动保存组件快照 +export async function updateComponentsLayer(payload: SaveCanvasComponentsPayload): Promise { + return getRequestClient().canvas.updateComponentsLayer({ + canvasId: payload.id, + components: payload.components, + }) +} + +// 更新画布背景图层(底图)属性 +export async function updateCanvasBaseLayer(payload: UpdateCanvasBaseLayerPayload): Promise { + return getRequestClient().canvas.updateBaseLayer({ + canvasId: payload.id, + width: payload.width, + height: payload.height, + thumbnail: payload.thumbnail, + lockAspectRatio: payload.lockAspectRatio, + backgroundColor: payload.backgroundColor, + }) +} diff --git a/packages/core/src/api/dynamic-project.ts b/packages/core/src/api/dynamic-project.ts new file mode 100644 index 0000000..dae5da5 --- /dev/null +++ b/packages/core/src/api/dynamic-project.ts @@ -0,0 +1,112 @@ +import { createHttpClient } from '@/request/client' + +export interface DynamicProjectModuleResponse { + module_pk: string + name: string + describe: string + label: string + classify: string +} + +export interface DynamicProjectModule extends DynamicProjectModuleResponse { + displayLabel: string + key: string +} + +export interface DynamicProjectModuleProp { + prop_pk: string + name: string + describe: string + t_module_prop: string + classify: string +} + +export interface DynamicProjectProperty extends DynamicProjectModuleProp { + displayLabel: string + key: string + defaultValue?: unknown +} + +const fetcher = createHttpClient() + +export async function getDynamicProjectModulesApi(baseProjectId: string): Promise { + if (!baseProjectId) { + return [] + } + + const data = await fetcher('/project/module/list/', { + method: 'GET', + headers: { + 'Hidden-Error': '1', + 'PROJECT': baseProjectId, + }, + query: { + pro: baseProjectId, + isEnum: 1, + }, + }) + + console.log('[dynamic-project] modules 原始响应', data) + + if (!Array.isArray(data)) { + console.warn('[dynamic-project] modules 响应非数组', { type: typeof data, data }) + return [] + } + + const result: DynamicProjectModule[] = data.map(module => ({ + ...module, + displayLabel: `${module.name} (${module.describe})`, + key: module.module_pk, + })) + + console.log('[dynamic-project] modules 归一化结果', result) + return result +} + +export async function getDynamicProjectModulePropsApi(baseProjectId: string, modulePk: string): Promise { + if (!baseProjectId || !modulePk) { + return [] + } + + const data = await fetcher<{ moduleProp: DynamicProjectModuleProp[] }>('/project/module/', { + method: 'GET', + headers: { + 'Hidden-Error': '1', + 'PROJECT': baseProjectId, + }, + query: { + pk: modulePk, + pro: baseProjectId, + isNeedFlow: 0, + }, + }) + + console.log(`[dynamic-project] module "${modulePk}" props 原始响应`, data) + + const propList = data?.moduleProp + if (!Array.isArray(propList)) { + console.warn(`[dynamic-project] module "${modulePk}" props 响应非数组`, { type: typeof propList, data }) + return [] + } + + // 按 name 去重:API 可能对同一属性返回多条记录,只保留首条 + const seen = new Set() + const result: DynamicProjectProperty[] = propList + .filter(prop => prop) + .filter((prop) => { + if (seen.has(prop.name)) { + return false + } + seen.add(prop.name) + return true + }) + .map(prop => ({ + ...prop, + displayLabel: `${prop.name} (${prop.describe})`, + key: prop.prop_pk, + defaultValue: undefined, + })) + + console.log(`[dynamic-project] module "${modulePk}" props 归一化结果`, { count: result.length }) + return result +} diff --git a/packages/core/src/api/index.ts b/packages/core/src/api/index.ts new file mode 100644 index 0000000..c36d721 --- /dev/null +++ b/packages/core/src/api/index.ts @@ -0,0 +1,4 @@ +export * from './canvas' +export * from './dynamic-project' +export * from './project' +export * from './runtime' diff --git a/packages/core/src/api/project.ts b/packages/core/src/api/project.ts new file mode 100644 index 0000000..1514047 --- /dev/null +++ b/packages/core/src/api/project.ts @@ -0,0 +1,15 @@ +import type { ProjectSummary } from '@/request' +import { requestClient } from '@/request' +import pinia, { useAppStore } from '@/stores' + +function getRequestClient() { + const { isWeb } = useAppStore(pinia) + if (!isWeb) + return requestClient.local + + return requestClient.http +} + +export async function getProjectListApi(): Promise { + return getRequestClient().project.list() +} diff --git a/packages/core/src/api/runtime.ts b/packages/core/src/api/runtime.ts new file mode 100644 index 0000000..ada423d --- /dev/null +++ b/packages/core/src/api/runtime.ts @@ -0,0 +1,99 @@ +import { ofetch } from 'ofetch' +import { createHttpClient } from '@/request/client' + +/** RPC 请求客户端(指向 chemical-chaos 运算服务,响应格式与 cslab-server 不同,不复用通用拦截器) */ +const rpcFetcher = ofetch.create({ + baseURL: '/chemical-chaos', + timeout: 600_000, +}) + +/** cslab-server 请求客户端(execSequence 等走 Django 后端) */ +const serverFetcher = createHttpClient() + +// ── 通用 RPC 调用 ── + +/** common/ 接口:返回格式为 { message, result } */ +function rpcCommon(clazz: string, method: string, kwargs: Record) { + return rpcFetcher('/v1/rpc/common/', { + method: 'POST', + body: { clazz, method, args: [], kwargs }, + }) +} + +/** zero_rpc/ 接口:返回格式为 { status, msg, data },自动解包取 data */ +async function rpcZero(method: string, kwargs: Record) { + const res = await rpcFetcher<{ status: number, msg: string, data: T }>('/v1/rpc/zero_rpc/', { + method: 'POST', + body: { method, kwargs }, + }) + return res.data +} + +// ── 执行顺序接口(cslab-server) ── + +/** 获取执行顺序(运行前的必要前置调用) */ +export function getExecSequenceApi(pro: string, callowWay: string) { + return serverFetcher('/project/module/execSequence/', { + method: 'GET', + params: { pro, is_preview: 0, callow_way: callowWay }, + headers: { PROJECT: pro }, + }) +} + +// ── 任务管理接口 ── + +export interface AddJobParams { + pro: string + addressee: number + callow_way: 'steady' | 'dynamic' | 'design' | 'chemicalPrinciple' + is_only_checked?: boolean + is_custom_sequence?: boolean + is_steady?: boolean + need_converge?: number + current_origin?: string + is_debug?: boolean + cal_label?: string + pk?: string[] +} + +export interface AddJobResult { + message: string + result: { + job: { id: string } + msg?: string + } +} + +/** 添加运行任务 */ +export function addJobApi(params: AddJobParams) { + return rpcCommon( + 'agent.rpc_client.run.run', + 'Run.add_job', + params as unknown as Record, + ) +} + +/** 暂停任务 */ +export function pauseJobApi(taskId: string, addressee: number) { + return rpcZero('pause_cal_job', { task_id: taskId, addressee }) +} + +/** 继续运行 */ +export function resumeJobApi(taskId: string, addressee: number) { + return rpcZero('unpause_cal_job', { task_id: taskId, addressee }) +} + +/** 停止任务 */ +export function stopJobApi(taskId: string, addressee: number) { + return rpcZero('exit_cal_job', { task_id: taskId, addressee }) +} + +/** 获取项目下的任务 */ +export function getProjectJobApi(addressee: number, pro: string) { + return rpcZero<{ state2: string | null, task: string }>('get_user_job', { addressee, pro }) +} + +/** 全量推送数据 */ +export function pushFullDataApi(taskId: string, addressee: number) { + return rpcZero('push_full_cal_data', { task_id: taskId, addressee }) +} diff --git a/packages/core/src/assets/fonts/Oxanium.woff2 b/packages/core/src/assets/fonts/Oxanium.woff2 new file mode 100644 index 0000000..e71b861 Binary files /dev/null and b/packages/core/src/assets/fonts/Oxanium.woff2 differ diff --git a/packages/core/src/assets/fonts/index.css b/packages/core/src/assets/fonts/index.css new file mode 100644 index 0000000..1204b9a --- /dev/null +++ b/packages/core/src/assets/fonts/index.css @@ -0,0 +1,6 @@ +@font-face { + font-family: 'Oxanium'; + src: url('./Oxanium.woff2') format('woff2'), + url('./Oxanium.woff') format('woff'); + font-weight: 400; +} \ No newline at end of file diff --git a/packages/core/src/assets/images/demo.png b/packages/core/src/assets/images/demo.png new file mode 100644 index 0000000..168fadb Binary files /dev/null and b/packages/core/src/assets/images/demo.png differ diff --git a/packages/core/src/assets/images/demo1.png b/packages/core/src/assets/images/demo1.png new file mode 100644 index 0000000..2847be5 Binary files /dev/null and b/packages/core/src/assets/images/demo1.png differ diff --git a/packages/core/src/assets/styles/element-plus-theme.scss b/packages/core/src/assets/styles/element-plus-theme.scss new file mode 100644 index 0000000..87947af --- /dev/null +++ b/packages/core/src/assets/styles/element-plus-theme.scss @@ -0,0 +1,227 @@ +@use "sass:color"; +@use "sass:math"; + +/* Element Plus 主题变量覆盖 */ +/* 基于 Element UI 变量转换为 Element Plus CSS Variables */ + +/* Transition +-------------------------- */ +$--all-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1) !default; +$--fade-transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1) !default; +$--fade-linear-transition: opacity 200ms linear !default; +$--md-fade-transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), + opacity 300ms cubic-bezier(0.23, 1, 0.32, 1) !default; +$--border-transition-base: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1) !default; +$--color-transition-base: color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1) !default; + +/* Color +-------------------------- */ +$--color-primary: #1677ff !default; +$--color-white: #ffffff !default; +$--color-black: #000000 !default; + +// Primary color variants +$--color-primary-light-1: color.mix($--color-white, $--color-primary, 10%) !default; +$--color-primary-light-2: color.mix($--color-white, $--color-primary, 20%) !default; +$--color-primary-light-3: color.mix($--color-white, $--color-primary, 30%) !default; +$--color-primary-light-4: color.mix($--color-white, $--color-primary, 40%) !default; +$--color-primary-light-5: color.mix($--color-white, $--color-primary, 50%) !default; +$--color-primary-light-6: color.mix($--color-white, $--color-primary, 60%) !default; +$--color-primary-light-7: color.mix($--color-white, $--color-primary, 70%) !default; +$--color-primary-light-8: color.mix($--color-white, $--color-primary, 80%) !default; +$--color-primary-light-9: color.mix($--color-white, $--color-primary, 90%) !default; + +// Functional colors +$--color-success: #0ac05e !default; +$--color-warning: #faad14 !default; +$--color-danger: #ff4d4f !default; +$--color-info: #8c8c8c !default; + +$--color-success-light: color.mix($--color-white, $--color-success, 80%) !default; +$--color-warning-light: color.mix($--color-white, $--color-warning, 80%) !default; +$--color-danger-light: color.mix($--color-white, $--color-danger, 80%) !default; +$--color-info-light: color.mix($--color-white, $--color-info, 80%) !default; + +$--color-success-lighter: color.mix($--color-white, $--color-success, 90%) !default; +$--color-warning-lighter: color.mix($--color-white, $--color-warning, 90%) !default; +$--color-danger-lighter: color.mix($--color-white, $--color-danger, 90%) !default; +$--color-info-lighter: color.mix($--color-white, $--color-info, 90%) !default; + +// Text colors +$--color-text-primary: #262626 !default; +$--color-text-regular: #595959 !default; +$--color-text-secondary: #8c8c8c !default; +$--color-text-placeholder: #bfbfbf !default; + +// Border colors +$--border-color-base: #d9d9d9 !default; +$--border-color-light: #e8e8e8 !default; +$--border-color-lighter: #e8e8e8 !default; +$--border-color-extra-light: #f5f5f5 !default; + +// Background +$--background-color-base: #f5f5f5 !default; + +/* Border +-------------------------- */ +$--border-width-base: 1px !default; +$--border-style-base: solid !default; +$--border-color-hover: $--color-text-placeholder !default; +$--border-base: $--border-width-base $--border-style-base $--border-color-base !default; +$--border-radius-base: 6px !default; +$--border-radius-small: 4px !default; +$--border-radius-circle: 100% !default; +$--border-radius-zero: 0 !default; + +/* Box-shadow +-------------------------- */ +$--box-shadow-base: 0 2px 8px rgba(0, 0, 0, 0.08) !default; +$--box-shadow-dark: 0 6px 16px rgba(0, 0, 0, 0.12) !default; +$--box-shadow-light: 0 4px 12px rgba(0, 0, 0, 0.1) !default; + +/* Typography +-------------------------- */ +$--font-size-extra-large: 20px !default; +$--font-size-large: 18px !default; +$--font-size-medium: 16px !default; +$--font-size-base: 14px !default; +$--font-size-small: 13px !default; +$--font-size-extra-small: 12px !default; +$--font-weight-primary: 500 !default; +$--font-weight-secondary: 100 !default; +$--font-line-height-primary: 24px !default; +$--font-line-height-secondary: 16px !default; + +/* z-index +-------------------------- */ +$--index-normal: 1 !default; +$--index-top: 1000 !default; +$--index-popper: 2000 !default; + +/* Disabled +-------------------------- */ +$--disabled-fill-base: $--background-color-base !default; +$--disabled-color-base: $--color-text-placeholder !default; +$--disabled-border-base: $--border-color-light !default; + +/* Element Plus CSS Variables Override +-------------------------- */ +:root { + // Colors + --el-color-primary: #{$--color-primary}; + --el-color-primary-light-3: #{$--color-primary-light-3}; + --el-color-primary-light-5: #{$--color-primary-light-5}; + --el-color-primary-light-7: #{$--color-primary-light-7}; + --el-color-primary-light-8: #{$--color-primary-light-8}; + --el-color-primary-light-9: #{$--color-primary-light-9}; + --el-color-primary-dark-2: #{color.mix($--color-black, $--color-primary, 20%)}; + + --el-color-success: #{$--color-success}; + --el-color-success-light-3: #{color.mix($--color-white, $--color-success, 30%)}; + --el-color-success-light-5: #{color.mix($--color-white, $--color-success, 50%)}; + --el-color-success-light-7: #{color.mix($--color-white, $--color-success, 70%)}; + --el-color-success-light-8: #{color.mix($--color-white, $--color-success, 80%)}; + --el-color-success-light-9: #{color.mix($--color-white, $--color-success, 90%)}; + + --el-color-warning: #{$--color-warning}; + --el-color-warning-light-3: #{color.mix($--color-white, $--color-warning, 30%)}; + --el-color-warning-light-5: #{color.mix($--color-white, $--color-warning, 50%)}; + --el-color-warning-light-7: #{color.mix($--color-white, $--color-warning, 70%)}; + --el-color-warning-light-8: #{color.mix($--color-white, $--color-warning, 80%)}; + --el-color-warning-light-9: #{color.mix($--color-white, $--color-warning, 90%)}; + + --el-color-danger: #{$--color-danger}; + --el-color-danger-light-3: #{color.mix($--color-white, $--color-danger, 30%)}; + --el-color-danger-light-5: #{color.mix($--color-white, $--color-danger, 50%)}; + --el-color-danger-light-7: #{color.mix($--color-white, $--color-danger, 70%)}; + --el-color-danger-light-8: #{color.mix($--color-white, $--color-danger, 80%)}; + --el-color-danger-light-9: #{color.mix($--color-white, $--color-danger, 90%)}; + + --el-color-info: #{$--color-info}; + --el-color-info-light-3: #{color.mix($--color-white, $--color-info, 30%)}; + --el-color-info-light-5: #{color.mix($--color-white, $--color-info, 50%)}; + --el-color-info-light-7: #{color.mix($--color-white, $--color-info, 70%)}; + --el-color-info-light-8: #{color.mix($--color-white, $--color-info, 80%)}; + --el-color-info-light-9: #{color.mix($--color-white, $--color-info, 90%)}; + + // Text colors + --el-text-color-primary: #{$--color-text-primary}; + --el-text-color-regular: #{$--color-text-regular}; + --el-text-color-secondary: #{$--color-text-secondary}; + --el-text-color-placeholder: #{$--color-text-placeholder}; + --el-text-color-disabled: #{$--disabled-color-base}; + + // Border colors + --el-border-color: #{$--border-color-base}; + --el-border-color-light: #{$--border-color-light}; + --el-border-color-lighter: #{$--border-color-lighter}; + --el-border-color-extra-light: #{$--border-color-extra-light}; + --el-border-color-dark: #{color.mix($--color-black, $--border-color-base, 20%)}; + --el-border-color-darker: #{color.mix($--color-black, $--border-color-base, 40%)}; + + // Fill colors + --el-fill-color: #{$--background-color-base}; + --el-fill-color-light: #{color.mix($--color-white, $--background-color-base, 30%)}; + --el-fill-color-lighter: #{color.mix($--color-white, $--background-color-base, 50%)}; + --el-fill-color-extra-light: #{color.mix($--color-white, $--background-color-base, 70%)}; + --el-fill-color-dark: #{color.mix($--color-black, $--background-color-base, 10%)}; + --el-fill-color-darker: #{color.mix($--color-black, $--background-color-base, 20%)}; + --el-fill-color-blank: #{$--color-white}; + + // Background colors + --el-bg-color: #{$--color-white}; + --el-bg-color-page: #{$--background-color-base}; + --el-bg-color-overlay: #{$--color-white}; + + // Border + --el-border-width: #{$--border-width-base}; + --el-border-style: #{$--border-style-base}; + --el-border-radius-base: #{$--border-radius-base}; + --el-border-radius-small: #{$--border-radius-small}; + --el-border-radius-round: 20px; + --el-border-radius-circle: #{$--border-radius-circle}; + + // Box-shadow + --el-box-shadow: #{$--box-shadow-base}; + --el-box-shadow-light: #{$--box-shadow-light}; + --el-box-shadow-lighter: 0 2px 4px rgba(0, 0, 0, 0.04); + --el-box-shadow-dark: #{$--box-shadow-dark}; + + // Typography + --el-font-size-extra-large: #{$--font-size-extra-large}; + --el-font-size-large: #{$--font-size-large}; + --el-font-size-medium: #{$--font-size-medium}; + --el-font-size-base: #{$--font-size-base}; + --el-font-size-small: #{$--font-size-small}; + --el-font-size-extra-small: #{$--font-size-extra-small}; + + --el-font-weight-primary: #{$--font-weight-primary}; + --el-font-line-height-primary: #{$--font-line-height-primary}; + + // z-index + --el-index-normal: #{$--index-normal}; + --el-index-top: #{$--index-top}; + --el-index-popper: #{$--index-popper}; + + // Transition + --el-transition-duration: 0.3s; + --el-transition-duration-fast: 0.2s; + --el-transition-function-ease-in-out-bezier: cubic-bezier(0.645, 0.045, 0.355, 1); + --el-transition-function-fast-bezier: cubic-bezier(0.23, 1, 0.32, 1); + --el-transition-all: #{$--all-transition}; + --el-transition-fade: #{$--fade-transition}; + --el-transition-fade-linear: #{$--fade-linear-transition}; + --el-transition-md-fade: #{$--md-fade-transition}; + --el-transition-border: #{$--border-transition-base}; + --el-transition-color: #{$--color-transition-base}; + + // Component size + --el-component-size-large: 40px; + --el-component-size: 32px; + --el-component-size-small: 24px; + + // Disabled + --el-disabled-bg-color: #{$--disabled-fill-base}; + --el-disabled-text-color: #{$--disabled-color-base}; + --el-disabled-border-color: #{$--disabled-border-base}; +} diff --git a/packages/core/src/assets/styles/index.scss b/packages/core/src/assets/styles/index.scss new file mode 100644 index 0000000..d25ab7e --- /dev/null +++ b/packages/core/src/assets/styles/index.scss @@ -0,0 +1,41 @@ +@use './variables.scss' as *; +@use './element-plus-theme.scss'; +@import url('../fonts/index.css'); + +/* Reset or Base styles */ +body { + background-color: $bg-color-page; + color: $text-color-primary; +} + +* { + box-sizing: border-box; +} + +/* Scrollbar customization */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: $border-color; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: $text-color-secondary; +} + +/* DCS 画布组件动画 */ +@keyframes dcs-blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.2; } +} +.dcs-blink { + animation: dcs-blink 1s ease-in-out infinite; +} \ No newline at end of file diff --git a/packages/core/src/assets/styles/variables.scss b/packages/core/src/assets/styles/variables.scss new file mode 100644 index 0000000..b82c6c9 --- /dev/null +++ b/packages/core/src/assets/styles/variables.scss @@ -0,0 +1,18 @@ +/* Global SCSS Variables */ +$primary-color: #1677ff; +$success-color: #0ac05e; +$warning-color: #faad14; +$danger-color: #ff4d4f; +$info-color: #8c8c8c; + +$text-color-primary: #262626; +$text-color-regular: #595959; +$text-color-secondary: #8c8c8c; +$text-color-placeholder: #bfbfbf; + +$border-color: #d9d9d9; +$border-color-light: #e8e8e8; +$border-color-lighter: #e8e8e8; + +$bg-color: #ffffff; +$bg-color-page: #f5f5f5; diff --git a/packages/core/src/auto-imports.d.ts b/packages/core/src/auto-imports.d.ts new file mode 100644 index 0000000..21b8307 --- /dev/null +++ b/packages/core/src/auto-imports.d.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// noinspection JSUnusedGlobalSymbols +// Generated by unplugin-auto-import +// biome-ignore lint: disable +export {} +declare global { + const ElMessage: typeof import('element-plus/es')['ElMessage'] + const ElMessageBox: typeof import('element-plus/es')['ElMessageBox'] +} diff --git a/packages/core/src/bootstrap/bridge.ts b/packages/core/src/bootstrap/bridge.ts new file mode 100644 index 0000000..3c8d1f0 --- /dev/null +++ b/packages/core/src/bootstrap/bridge.ts @@ -0,0 +1,23 @@ +import type { IPlatformBridge } from '@cslab-dcs/bridge' +import { bridge as defaultBridge } from '@cslab-dcs/bridge' +import { inject, onMounted } from 'vue' +import pinia from '@/stores' +import { useAppStore } from '@/stores/app' + +export function useBridge() { + const bridge = inject('bridge', defaultBridge) + + const appStore = useAppStore(pinia) + + onMounted(async () => { + const platformInfo = await bridge.system.getPlatformInfo() + appStore.setPlatformInfo(platformInfo) + + const appVersion = await bridge.system.getAppVersion() + appStore.setAppVersion(appVersion) + }) + + return { + bridge, + } +} diff --git a/packages/core/src/bootstrap/editor.ts b/packages/core/src/bootstrap/editor.ts new file mode 100644 index 0000000..3e18e8d --- /dev/null +++ b/packages/core/src/bootstrap/editor.ts @@ -0,0 +1,64 @@ +import { storeToRefs } from 'pinia' +import { watch } from 'vue' +import pinia, { useCanvasStore, useLayerStore, useProjectStore } from '@/stores' + +export function useEditorWatchEffect() { + const projectStore = useProjectStore(pinia) + const { projectId } = storeToRefs(projectStore) + + const canvasStore = useCanvasStore(pinia) + const { canvasId } = storeToRefs(canvasStore) + + const layerStore = useLayerStore(pinia) + + function resetCanvas() { + canvasStore.setCanvasId(null) + canvasStore.setCanvasList([]) + } + + async function refreshCanvasAndLayer(preferredCanvasId?: string | null) { + const canvases = await canvasStore.getCanvasList() + const selectedCanvas = preferredCanvasId + ? canvases.find(canvas => canvas.id === preferredCanvasId) + : undefined + const nextCanvas = selectedCanvas || canvases[0] || null + canvasStore.setCanvasId(nextCanvas?.id || null) + } + + watch( + () => projectId.value, + async (nextProjectId) => { + // 切换项目时重置画布和图层状态 + const preferredCanvasId = canvasId.value + resetCanvas() + + if (!nextProjectId) { + return + } + + // 获取新项目的项目信息,更新项目名称等相关状态 + await projectStore.getProjectInfo() + + // 获取新项目的画布列表,并尽量保留 URL / store 中指定的画布 + await refreshCanvasAndLayer(preferredCanvasId) + }, + { immediate: true }, + ) + + function resetLayer() { + layerStore.setLayerId('') + layerStore.replaceLayers([]) + } + + watch( + () => canvasId.value, + async () => { + // 切换画布时重置图层状态 + resetLayer() + + // 获取新画布的信息 + await canvasStore.getCanvasInfo() + }, + { immediate: true }, + ) +} diff --git a/packages/core/src/bootstrap/index.ts b/packages/core/src/bootstrap/index.ts new file mode 100644 index 0000000..d71ae14 --- /dev/null +++ b/packages/core/src/bootstrap/index.ts @@ -0,0 +1,3 @@ +export * from './bridge' +export * from './editor' +export * from './route-state' diff --git a/packages/core/src/bootstrap/route-state.ts b/packages/core/src/bootstrap/route-state.ts new file mode 100644 index 0000000..47e0709 --- /dev/null +++ b/packages/core/src/bootstrap/route-state.ts @@ -0,0 +1,52 @@ +import { storeToRefs } from 'pinia' +import { watch } from 'vue' +import { useRoute, useRouter } from 'vue-router' +import { getRouteCanvasId, getRouteProjectId, mergeRouteStateQuery, shouldAttachCanvasId } from '@/router/route-state' +import { useCanvasStore, useProjectStore } from '@/stores' + +export function useRouteStateSync() { + const route = useRoute() + const router = useRouter() + + const projectStore = useProjectStore() + const canvasStore = useCanvasStore() + + const { projectId } = storeToRefs(projectStore) + const { canvasId } = storeToRefs(canvasStore) + + watch( + [projectId, canvasId, () => route.fullPath], + async () => { + const nextProjectId = projectId.value || '' + const nextCanvasId = canvasId.value || '' + const routeProjectId = getRouteProjectId(route.query) + const routeCanvasId = getRouteCanvasId(route.query) + const includeCanvasId = shouldAttachCanvasId(route) + + // 首次刷新时优先从当前 URL 恢复上下文,避免 store 尚未就绪时把 query 擦掉。 + if (!nextProjectId && routeProjectId) { + projectStore.setProjectId(routeProjectId) + return + } + + if (includeCanvasId && !nextCanvasId && routeCanvasId) { + canvasStore.setCanvasId(routeCanvasId) + return + } + + if (routeProjectId === nextProjectId && routeCanvasId === nextCanvasId) { + return + } + + await router.replace({ + path: route.path, + query: mergeRouteStateQuery(route.query, { + projectId: nextProjectId, + canvasId: nextCanvasId, + }, { includeCanvasId }), + hash: route.hash, + }) + }, + { immediate: true }, + ) +} diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts new file mode 100644 index 0000000..7ecee07 --- /dev/null +++ b/packages/core/src/components.d.ts @@ -0,0 +1,41 @@ +/* eslint-disable */ +// @ts-nocheck +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 +// biome-ignore lint: disable +export {} + +/* prettier-ignore */ +declare module 'vue' { + export interface GlobalComponents { + ElAlert: typeof import('element-plus/es')['ElAlert'] + ElBadge: typeof import('element-plus/es')['ElBadge'] + ElButton: typeof import('element-plus/es')['ElButton'] + ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup'] + ElCard: typeof import('element-plus/es')['ElCard'] + ElCascader: typeof import('element-plus/es')['ElCascader'] + ElCol: typeof import('element-plus/es')['ElCol'] + ElColorPicker: typeof import('element-plus/es')['ElColorPicker'] + ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] + ElDialog: typeof import('element-plus/es')['ElDialog'] + ElDropdown: typeof import('element-plus/es')['ElDropdown'] + ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] + ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] + ElForm: typeof import('element-plus/es')['ElForm'] + ElFormItem: typeof import('element-plus/es')['ElFormItem'] + ElInput: typeof import('element-plus/es')['ElInput'] + ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] + ElOption: typeof import('element-plus/es')['ElOption'] + ElOptionGroup: typeof import('element-plus/es')['ElOptionGroup'] + ElPopover: typeof import('element-plus/es')['ElPopover'] + ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] + ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] + ElRow: typeof import('element-plus/es')['ElRow'] + ElSelect: typeof import('element-plus/es')['ElSelect'] + ElSwitch: typeof import('element-plus/es')['ElSwitch'] + ElTag: typeof import('element-plus/es')['ElTag'] + ElUpload: typeof import('element-plus/es')['ElUpload'] + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + } +} diff --git a/packages/core/src/components/editor/canvas/components/page-form-dialog.vue b/packages/core/src/components/editor/canvas/components/page-form-dialog.vue new file mode 100644 index 0000000..4a3d910 --- /dev/null +++ b/packages/core/src/components/editor/canvas/components/page-form-dialog.vue @@ -0,0 +1,135 @@ + + + diff --git a/packages/core/src/components/editor/canvas/composables/layer.domain.ts b/packages/core/src/components/editor/canvas/composables/layer.domain.ts new file mode 100644 index 0000000..64890a3 --- /dev/null +++ b/packages/core/src/components/editor/canvas/composables/layer.domain.ts @@ -0,0 +1,779 @@ +import type { Layer } from '../types' +import type { RectLike } from '../utils' +import { createLayerGroupId, getLayerGroupMeta, withLayerGroupMeta } from '../grouping' +import { rectIntersects } from '../utils' + +export interface LayerTransformResult { + changed: boolean + nextLayers: Layer[] + groupId?: string | null +} + +export interface LayerOrderItem { + kind: 'group' | 'component' + id: string + groupId?: string +} + +export type LayerMovePlacement = 'before' | 'after' | 'into' +export type LayerMoveEdge = 'front' | 'back' +export type LayerMoveDirection = 'forward' | 'backward' + +const GROUP_NAME_PATTERN = /^分组\s*(\d+)$/ + +function uniqueLayerIds(layerIds: string[]) { + return [...new Set(layerIds)].filter(Boolean) +} + +export function canGroupLayersByIds(layers: Layer[], layerIds: string[]) { + const selectedIds = uniqueLayerIds(layerIds) + if (selectedIds.length < 1) { + return false + } + + const selectedLayers = layers.filter(layer => selectedIds.includes(layer.id)) + if (selectedLayers.length !== selectedIds.length) { + return false + } + + return selectedLayers.every(layer => !getLayerGroupMeta(layer)) +} + +function isSameOrder(left: Layer[], right: Layer[]) { + if (left.length !== right.length) { + return false + } + return left.every((item, index) => item.id === right[index]?.id) +} + +interface LayerOrderBlock { + kind: 'group' | 'component' + id: string + groupId?: string + layerIds: string[] +} + +interface OrderedEntryWithBounds { + entry: T + bounds: RectLike +} + +function resolveOrderItemKey(item: LayerOrderItem | LayerOrderBlock) { + return item.kind === 'group' + ? `group:${item.groupId || item.id}` + : item.id +} + +function resolveOrderItemParentKey(item: LayerOrderItem) { + if (item.kind === 'group') { + return 'root' + } + return item.groupId ? `group:${item.groupId}` : 'root' +} + +function normalizeLayerOrderItems(items: LayerOrderItem[]) { + const uniqueItems: LayerOrderItem[] = [] + const seenKeys = new Set() + + for (const item of items) { + const key = resolveOrderItemKey(item) + if (seenKeys.has(key)) { + continue + } + seenKeys.add(key) + uniqueItems.push(item) + } + + if (!uniqueItems.length) { + return null + } + + const parentKey = resolveOrderItemParentKey(uniqueItems[0]) + // 只允许同一层级内一起移动,避免根节点和组内节点混排造成顺序错乱。 + if (uniqueItems.some(item => resolveOrderItemParentKey(item) !== parentKey)) { + return null + } + + return { + items: uniqueItems, + parentKey, + selectedKeys: new Set(uniqueItems.map(item => resolveOrderItemKey(item))), + } +} + +function buildLayerOrderBlocks(layers: Layer[]) { + const groupBlocks = new Map() + const blocks: LayerOrderBlock[] = [] + + layers.forEach((layer) => { + const group = getLayerGroupMeta(layer) + if (!group) { + blocks.push({ + kind: 'component', + id: layer.id, + layerIds: [layer.id], + }) + return + } + + const existingBlock = groupBlocks.get(group.groupId) + if (existingBlock) { + // 分组在图层排序里视为一个整体块,内部成员顺序单独维护。 + existingBlock.layerIds.push(layer.id) + return + } + + const nextBlock: LayerOrderBlock = { + kind: 'group', + id: `group:${group.groupId}`, + groupId: group.groupId, + layerIds: [layer.id], + } + groupBlocks.set(group.groupId, nextBlock) + blocks.push(nextBlock) + }) + + return blocks +} + +function mapLayersById(layers: Layer[]) { + return new Map(layers.map(layer => [layer.id, layer] as const)) +} + +function flattenBlocksToLayers(layers: Layer[], blocks: LayerOrderBlock[]) { + const layerMap = mapLayersById(layers) + return blocks.flatMap(block => block.layerIds.map(layerId => layerMap.get(layerId)).filter((layer): layer is Layer => Boolean(layer))) +} + +function resolveLayerBounds(layer: Layer): RectLike { + return { + x: layer.x, + y: layer.y, + width: layer.width, + height: layer.height, + } +} + +function mergeRectBounds(boundsList: RectLike[]) { + const [firstBound, ...restBounds] = boundsList + if (!firstBound) { + return null + } + + let left = firstBound.x + let top = firstBound.y + let right = firstBound.x + firstBound.width + let bottom = firstBound.y + firstBound.height + + restBounds.forEach((bound) => { + left = Math.min(left, bound.x) + top = Math.min(top, bound.y) + right = Math.max(right, bound.x + bound.width) + bottom = Math.max(bottom, bound.y + bound.height) + }) + + return { + x: left, + y: top, + width: right - left, + height: bottom - top, + } +} + +function resolveBlockBounds(layers: Layer[], block: LayerOrderBlock) { + const layerMap = mapLayersById(layers) + const boundsList = block.layerIds + .map(layerId => layerMap.get(layerId)) + .filter((layer): layer is Layer => Boolean(layer)) + .map(resolveLayerBounds) + + return mergeRectBounds(boundsList) +} + +function moveSelectedEntriesToEdge( + items: T[], + isSelected: (item: T) => boolean, + edge: LayerMoveEdge, +) { + const selected: T[] = [] + const unselected: T[] = [] + + items.forEach((item) => { + if (isSelected(item)) { + selected.push(item) + return + } + unselected.push(item) + }) + + if (!selected.length || !unselected.length) { + return items + } + + return edge === 'back' + ? [...selected, ...unselected] + : [...unselected, ...selected] +} + +function moveSelectedEntriesByStep( + items: T[], + isSelected: (item: T) => boolean, + direction: LayerMoveDirection, +) { + const nextItems = [...items] + + if (direction === 'forward') { + for (let index = nextItems.length - 2; index >= 0; index -= 1) { + if (!isSelected(nextItems[index]) || isSelected(nextItems[index + 1])) { + continue + } + ;[nextItems[index], nextItems[index + 1]] = [nextItems[index + 1], nextItems[index]] + } + return nextItems + } + + for (let index = 1; index < nextItems.length; index += 1) { + if (!isSelected(nextItems[index]) || isSelected(nextItems[index - 1])) { + continue + } + ;[nextItems[index - 1], nextItems[index]] = [nextItems[index], nextItems[index - 1]] + } + + return nextItems +} + +function moveSelectedEntriesAroundTarget( + items: T[], + isSelected: (item: T) => boolean, + targetIndex: number, + direction: LayerMoveDirection, +) { + const nextItems = [...items] + const selectedEntries = nextItems.filter(isSelected) + if (!selectedEntries.length || targetIndex < 0) { + return items + } + + const remainingItems = nextItems.filter(item => !isSelected(item)) + const targetEntry = items[targetIndex] + const nextTargetIndex = remainingItems.findIndex(item => item === targetEntry) + if (nextTargetIndex < 0) { + return items + } + + const insertionIndex = direction === 'forward' ? nextTargetIndex + 1 : nextTargetIndex + remainingItems.splice(insertionIndex, 0, ...selectedEntries) + return remainingItems +} + +function findClosestOverlapTargetIndex( + entries: Array>, + isSelected: (entry: T) => boolean, + direction: LayerMoveDirection, +) { + const selectedEntries = entries.filter(item => isSelected(item.entry)) + if (!selectedEntries.length) { + return null + } + + const selectedBounds = selectedEntries.map(item => item.bounds) + const searchStart = direction === 'forward' + ? Math.max(...entries.map((item, index) => (isSelected(item.entry) ? index : -1))) + : Math.min(...entries.map((item, index) => (isSelected(item.entry) ? index : Number.POSITIVE_INFINITY))) + + if (!Number.isFinite(searchStart)) { + return null + } + + if (direction === 'forward') { + // 优先寻找与选中块几何上相交的目标,移动体验更符合“视觉前后”的直觉。 + for (let index = searchStart + 1; index < entries.length; index += 1) { + const candidate = entries[index] + if (isSelected(candidate.entry)) { + continue + } + if (selectedBounds.some(bound => rectIntersects(bound, candidate.bounds))) { + return index + } + } + return null + } + + for (let index = searchStart - 1; index >= 0; index -= 1) { + const candidate = entries[index] + if (isSelected(candidate.entry)) { + continue + } + if (selectedBounds.some(bound => rectIntersects(bound, candidate.bounds))) { + return index + } + } + + return null +} + +function resolveNextGroupName(layers: Layer[]) { + // 复用“分组N”序列,避免每次新建都出现随机名。 + const maxGroupIndex = layers.reduce((max, layer) => { + const groupName = getLayerGroupMeta(layer)?.groupName?.trim() + if (!groupName) { + return max + } + + const matched = GROUP_NAME_PATTERN.exec(groupName) + if (!matched) { + return max + } + + const value = Number.parseInt(matched[1], 10) + return Number.isFinite(value) ? Math.max(max, value) : max + }, 0) + + return `分组${maxGroupIndex + 1}` +} + +export function reorderLayersToFront(layers: Layer[], layerIds: string[]): LayerTransformResult { + const selectedSet = new Set(uniqueLayerIds(layerIds)) + if (!selectedSet.size) { + return { changed: false, nextLayers: layers } + } + + const selected: Layer[] = [] + const unselected: Layer[] = [] + layers.forEach((layer) => { + if (selectedSet.has(layer.id)) { + selected.push(layer) + return + } + unselected.push(layer) + }) + + if (!selected.length) { + return { changed: false, nextLayers: layers } + } + + const nextLayers = [...unselected, ...selected] + return { + changed: !isSameOrder(layers, nextLayers), + nextLayers, + } +} + +export function reorderLayersToBack(layers: Layer[], layerIds: string[]): LayerTransformResult { + const selectedSet = new Set(uniqueLayerIds(layerIds)) + if (!selectedSet.size) { + return { changed: false, nextLayers: layers } + } + + const selected: Layer[] = [] + const unselected: Layer[] = [] + layers.forEach((layer) => { + if (selectedSet.has(layer.id)) { + selected.push(layer) + return + } + unselected.push(layer) + }) + + if (!selected.length) { + return { changed: false, nextLayers: layers } + } + + const nextLayers = [...selected, ...unselected] + return { + changed: !isSameOrder(layers, nextLayers), + nextLayers, + } +} + +export function reorderLayerBefore(layers: Layer[], sourceId: string, targetId: string): LayerTransformResult { + if (!sourceId || !targetId || sourceId === targetId) { + return { changed: false, nextLayers: layers } + } + + const sourceIndex = layers.findIndex(layer => layer.id === sourceId) + const targetIndex = layers.findIndex(layer => layer.id === targetId) + if (sourceIndex < 0 || targetIndex < 0) { + return { changed: false, nextLayers: layers } + } + + const nextLayers = [...layers] + const [sourceLayer] = nextLayers.splice(sourceIndex, 1) + if (!sourceLayer) { + return { changed: false, nextLayers: layers } + } + + const nextTargetIndex = sourceIndex < targetIndex ? targetIndex - 1 : targetIndex + nextLayers.splice(nextTargetIndex, 0, sourceLayer) + return { + changed: !isSameOrder(layers, nextLayers), + nextLayers, + } +} + +// 跨父级移动:组件在根级 / 组之间自由拖拽。 +function moveLayerOrderItemCrossParent( + layers: Layer[], + source: LayerOrderItem, + target: LayerOrderItem, + placement: LayerMovePlacement, +): LayerTransformResult { + // 只有组件才允许跨组移动,分组整体不能被拖入另一个分组。 + if (source.kind !== 'component') { + return { changed: false, nextLayers: layers } + } + + const sourceLayer = layers.find(l => l.id === source.id) + if (!sourceLayer) { + return { changed: false, nextLayers: layers } + } + + // 确定目标分组:into → 进入目标组;拖到组成员旁 → 加入该组;否则 → 脱组到根级。 + let targetGroupMeta: { groupId: string, groupName: string } | null = null + + if (placement === 'into' && target.kind === 'group' && target.groupId) { + const member = layers.find(l => getLayerGroupMeta(l)?.groupId === target.groupId) + const meta = member ? getLayerGroupMeta(member) : null + if (meta) { + targetGroupMeta = { groupId: meta.groupId, groupName: meta.groupName } + } + } + else if (target.kind === 'component' && target.groupId) { + const targetLayer = layers.find(l => l.id === target.id) + const meta = targetLayer ? getLayerGroupMeta(targetLayer) : null + if (meta) { + targetGroupMeta = { groupId: meta.groupId, groupName: meta.groupName } + } + } + + const updatedSource = withLayerGroupMeta(sourceLayer, targetGroupMeta) + const withoutSource = layers.filter(l => l.id !== source.id) + + let insertionIndex: number + + if (placement === 'into' && target.kind === 'group' && target.groupId) { + // 放入组末尾(数组末端 = 面板中组内最上方 = 最高 z-index) + let lastIdx = -1 + withoutSource.forEach((l, i) => { + if (getLayerGroupMeta(l)?.groupId === target.groupId) { + lastIdx = i + } + }) + insertionIndex = lastIdx >= 0 ? lastIdx + 1 : withoutSource.length + } + else if (target.kind === 'group' && target.groupId) { + // 拖到组头行的 before / after → 根级排序,图层不进组 + const groupIndices: number[] = [] + withoutSource.forEach((l, i) => { + if (getLayerGroupMeta(l)?.groupId === target.groupId) { + groupIndices.push(i) + } + }) + if (!groupIndices.length) { + return { changed: false, nextLayers: layers } + } + insertionIndex = placement === 'before' + ? Math.min(...groupIndices) + : Math.max(...groupIndices) + 1 + } + else { + // 拖到普通组件旁 + const targetIdx = withoutSource.findIndex(l => l.id === target.id) + if (targetIdx < 0) { + return { changed: false, nextLayers: layers } + } + insertionIndex = placement === 'before' ? targetIdx : targetIdx + 1 + } + + const nextLayers = [...withoutSource] + nextLayers.splice(insertionIndex, 0, updatedSource) + + return { + changed: !isSameOrder(layers, nextLayers), + nextLayers, + } +} + +export function moveLayerOrderItem( + layers: Layer[], + source: LayerOrderItem, + target: LayerOrderItem, + placement: LayerMovePlacement, +): LayerTransformResult { + if (!source.id || !target.id) { + return { changed: false, nextLayers: layers } + } + + const sourceParentKey = resolveOrderItemParentKey(source) + const targetParentKey = resolveOrderItemParentKey(target) + + // 跨父级移动或 into 放入分组 + if (sourceParentKey !== targetParentKey || placement === 'into') { + return moveLayerOrderItemCrossParent(layers, source, target, placement) + } + + const blocks = buildLayerOrderBlocks(layers) + + if (sourceParentKey === 'root') { + // 根层级拖拽时,分组整体和独立图层都作为 block 参与排序。 + const sourceKey = resolveOrderItemKey(source) + const targetKey = resolveOrderItemKey(target) + if (sourceKey === targetKey) { + return { changed: false, nextLayers: layers } + } + + const sourceIndex = blocks.findIndex(block => resolveOrderItemKey(block) === sourceKey) + const targetIndex = blocks.findIndex(block => resolveOrderItemKey(block) === targetKey) + if (sourceIndex < 0 || targetIndex < 0) { + return { changed: false, nextLayers: layers } + } + + const nextBlocks = [...blocks] + const [sourceBlock] = nextBlocks.splice(sourceIndex, 1) + if (!sourceBlock) { + return { changed: false, nextLayers: layers } + } + + const nextTargetIndex = nextBlocks.findIndex(block => resolveOrderItemKey(block) === targetKey) + if (nextTargetIndex < 0) { + return { changed: false, nextLayers: layers } + } + + const insertionIndex = placement === 'before' ? nextTargetIndex : nextTargetIndex + 1 + nextBlocks.splice(insertionIndex, 0, sourceBlock) + const nextLayers = flattenBlocksToLayers(layers, nextBlocks) + + return { + changed: !isSameOrder(layers, nextLayers), + nextLayers, + } + } + + if (source.kind !== 'component' || target.kind !== 'component' || !source.groupId || source.groupId !== target.groupId) { + return { changed: false, nextLayers: layers } + } + + const targetGroupIndex = blocks.findIndex(block => block.kind === 'group' && block.groupId === source.groupId) + if (targetGroupIndex < 0) { + return { changed: false, nextLayers: layers } + } + + const targetGroupBlock = blocks[targetGroupIndex] + const sourceIndex = targetGroupBlock.layerIds.findIndex(layerId => layerId === source.id) + const targetIndex = targetGroupBlock.layerIds.findIndex(layerId => layerId === target.id) + if (sourceIndex < 0 || targetIndex < 0 || source.id === target.id) { + return { changed: false, nextLayers: layers } + } + + const nextGroupLayerIds = [...targetGroupBlock.layerIds] + const [sourceLayerId] = nextGroupLayerIds.splice(sourceIndex, 1) + if (!sourceLayerId) { + return { changed: false, nextLayers: layers } + } + + const nextTargetIndex = nextGroupLayerIds.findIndex(layerId => layerId === target.id) + if (nextTargetIndex < 0) { + return { changed: false, nextLayers: layers } + } + + const insertionIndex = placement === 'before' ? nextTargetIndex : nextTargetIndex + 1 + nextGroupLayerIds.splice(insertionIndex, 0, sourceLayerId) + const nextBlocks = [...blocks] + nextBlocks[targetGroupIndex] = { + ...targetGroupBlock, + layerIds: nextGroupLayerIds, + } + const nextLayers = flattenBlocksToLayers(layers, nextBlocks) + + return { + changed: !isSameOrder(layers, nextLayers), + nextLayers, + } +} + +export function moveLayerOrderItemsToEdge( + layers: Layer[], + selectedItems: LayerOrderItem[], + edge: LayerMoveEdge, +): LayerTransformResult { + const normalized = normalizeLayerOrderItems(selectedItems) + if (!normalized) { + return { changed: false, nextLayers: layers } + } + + const blocks = buildLayerOrderBlocks(layers) + const isSelectedBlock = (block: LayerOrderBlock) => normalized.selectedKeys.has(resolveOrderItemKey(block)) + + if (normalized.parentKey === 'root') { + const nextBlocks = moveSelectedEntriesToEdge(blocks, isSelectedBlock, edge) + const nextLayers = flattenBlocksToLayers(layers, nextBlocks) + return { + changed: !isSameOrder(layers, nextLayers), + nextLayers, + } + } + + const groupId = normalized.items[0]?.groupId + if (!groupId) { + return { changed: false, nextLayers: layers } + } + + const targetGroupIndex = blocks.findIndex(block => block.kind === 'group' && block.groupId === groupId) + if (targetGroupIndex < 0) { + return { changed: false, nextLayers: layers } + } + + const targetGroupBlock = blocks[targetGroupIndex] + const nextGroupLayerIds = moveSelectedEntriesToEdge( + targetGroupBlock.layerIds, + layerId => normalized.selectedKeys.has(layerId), + edge, + ) + const nextBlocks = [...blocks] + nextBlocks[targetGroupIndex] = { + ...targetGroupBlock, + layerIds: nextGroupLayerIds, + } + const nextLayers = flattenBlocksToLayers(layers, nextBlocks) + + return { + changed: !isSameOrder(layers, nextLayers), + nextLayers, + } +} + +export function moveLayerOrderItemsByStep( + layers: Layer[], + selectedItems: LayerOrderItem[], + direction: LayerMoveDirection, +): LayerTransformResult { + const normalized = normalizeLayerOrderItems(selectedItems) + if (!normalized) { + return { changed: false, nextLayers: layers } + } + + const blocks = buildLayerOrderBlocks(layers) + const isSelectedBlock = (block: LayerOrderBlock) => normalized.selectedKeys.has(resolveOrderItemKey(block)) + + if (normalized.parentKey === 'root') { + // 根层级优先按几何重叠关系找“下一站”,否则退回到纯顺序移动。 + const rootEntries = blocks + .map(block => ({ + entry: block, + bounds: resolveBlockBounds(layers, block), + })) + .filter((item): item is OrderedEntryWithBounds => Boolean(item.bounds)) + const overlapTargetIndex = findClosestOverlapTargetIndex(rootEntries, isSelectedBlock, direction) + const nextBlocks = overlapTargetIndex === null + ? moveSelectedEntriesByStep(blocks, isSelectedBlock, direction) + : moveSelectedEntriesAroundTarget(blocks, isSelectedBlock, overlapTargetIndex, direction) + const nextLayers = flattenBlocksToLayers(layers, nextBlocks) + return { + changed: !isSameOrder(layers, nextLayers), + nextLayers, + } + } + + const groupId = normalized.items[0]?.groupId + if (!groupId) { + return { changed: false, nextLayers: layers } + } + + const targetGroupIndex = blocks.findIndex(block => block.kind === 'group' && block.groupId === groupId) + if (targetGroupIndex < 0) { + return { changed: false, nextLayers: layers } + } + + const targetGroupBlock = blocks[targetGroupIndex] + const layerMap = mapLayersById(layers) + const groupEntries = targetGroupBlock.layerIds + .map((layerId) => { + const layer = layerMap.get(layerId) + if (!layer) { + return null + } + return { + entry: layerId, + bounds: resolveLayerBounds(layer), + } + }) + .filter((item): item is OrderedEntryWithBounds => Boolean(item)) + const overlapTargetIndex = findClosestOverlapTargetIndex( + groupEntries, + layerId => normalized.selectedKeys.has(layerId), + direction, + ) + const nextGroupLayerIds = overlapTargetIndex === null + ? moveSelectedEntriesByStep( + targetGroupBlock.layerIds, + layerId => normalized.selectedKeys.has(layerId), + direction, + ) + : moveSelectedEntriesAroundTarget( + targetGroupBlock.layerIds, + layerId => normalized.selectedKeys.has(layerId), + overlapTargetIndex, + direction, + ) + const nextBlocks = [...blocks] + nextBlocks[targetGroupIndex] = { + ...targetGroupBlock, + layerIds: nextGroupLayerIds, + } + const nextLayers = flattenBlocksToLayers(layers, nextBlocks) + + return { + changed: !isSameOrder(layers, nextLayers), + nextLayers, + } +} + +export function groupLayersByIds(layers: Layer[], layerIds: string[], groupName?: string): LayerTransformResult { + const selectedIds = uniqueLayerIds(layerIds) + if (!canGroupLayersByIds(layers, selectedIds)) { + return { changed: false, nextLayers: layers } + } + + const selectedSet = new Set(selectedIds) + const groupId = createLayerGroupId() + const nextGroupName = groupName?.trim() || resolveNextGroupName(layers) + const selectedLayers = layers + .filter(layer => selectedSet.has(layer.id)) + .map(layer => withLayerGroupMeta(layer, { groupId, groupName: nextGroupName })) + if (selectedLayers.length < 1) { + return { changed: false, nextLayers: layers } + } + + const firstSelectedIndex = layers.findIndex(layer => selectedSet.has(layer.id)) + const unselectedLayers = layers.filter(layer => !selectedSet.has(layer.id)) + // 分组后把整组插回首次选中位置,尽量保持用户原有层级感知。 + const nextLayers = [...unselectedLayers] + nextLayers.splice(firstSelectedIndex, 0, ...selectedLayers) + + return { + changed: true, + nextLayers, + groupId, + } +} + +export function ungroupLayersByIds(layers: Layer[], layerIds?: string[]): LayerTransformResult { + const targetSet = layerIds?.length ? new Set(uniqueLayerIds(layerIds)) : null + let changed = false + + const nextLayers = layers.map((layer) => { + if (targetSet && !targetSet.has(layer.id)) { + return layer + } + if (!getLayerGroupMeta(layer)) { + return layer + } + changed = true + return withLayerGroupMeta(layer, null) + }) + + return { + changed, + nextLayers, + } +} diff --git a/packages/core/src/components/editor/canvas/composables/layer.ts b/packages/core/src/components/editor/canvas/composables/layer.ts new file mode 100644 index 0000000..3dc9139 --- /dev/null +++ b/packages/core/src/components/editor/canvas/composables/layer.ts @@ -0,0 +1,543 @@ +import type { PointerPosition } from '../context' +import type { Layer } from '../types' +import type { LayerMoveDirection, LayerMoveEdge, LayerMovePlacement, LayerOrderItem } from './layer.domain' +import { GRID_SIZE } from '@/constants' +import { useLayerStore } from '@/stores' +import { useCanvasState } from '../context' +import { getLayerGroupById, getLayerGroupMeta } from '../grouping' +import { clamp, createLayerId, snapToGrid } from '../utils' +import { + canGroupLayersByIds, + groupLayersByIds, + moveLayerOrderItem, + moveLayerOrderItemsByStep, + moveLayerOrderItemsToEdge, + reorderLayerBefore, + reorderLayersToBack, + reorderLayersToFront, + ungroupLayersByIds, +} from './layer.domain' + +export function useLayerActions() { + const { layerList } = useCanvasState() + const layerStore = useLayerStore() + + function applyLayerList(nextLayers: Layer[]) { + // 图层顺序和分组调整后保留当前选区,避免用户每次操作都丢选中态。 + layerStore.setLayerList(nextLayers, { preserveSelection: true }) + } + + function toggleLayerLock(id: string, locked?: boolean) { + return layerStore.updateLayerById(id, (item) => { + const nextLocked = typeof locked === 'boolean' ? locked : !item.config?.locked + return { + ...item, + config: { + ...item.config, + locked: nextLocked, + }, + } + }) + } + + function toggleLayerVisible(id: string, visible?: boolean) { + return layerStore.updateLayerById(id, (item) => { + const nextVisible = typeof visible === 'boolean' ? visible : item.config?.visible === false + return { + ...item, + config: { + ...item.config, + visible: nextVisible, + }, + } + }) + } + + function bringLayersToFront(layerIds: string[]) { + const { changed, nextLayers } = reorderLayersToFront(layerList.value, layerIds) + if (!changed) { + return false + } + applyLayerList(nextLayers) + return true + } + + function sendLayersToBack(layerIds: string[]) { + const { changed, nextLayers } = reorderLayersToBack(layerList.value, layerIds) + if (!changed) { + return false + } + applyLayerList(nextLayers) + return true + } + + function moveLayerBefore(sourceId: string, targetId: string) { + const { changed, nextLayers } = reorderLayerBefore(layerList.value, sourceId, targetId) + if (!changed) { + return false + } + applyLayerList(nextLayers) + return true + } + + function moveLayerItem(source: LayerOrderItem, target: LayerOrderItem, placement: LayerMovePlacement) { + const { changed, nextLayers } = moveLayerOrderItem(layerList.value, source, target, placement) + if (!changed) { + return false + } + applyLayerList(nextLayers) + return true + } + + function moveLayerItemsToEdge(items: LayerOrderItem[], edge: LayerMoveEdge) { + const { changed, nextLayers } = moveLayerOrderItemsToEdge(layerList.value, items, edge) + if (!changed) { + return false + } + applyLayerList(nextLayers) + return true + } + + function moveLayerItemsByStep(items: LayerOrderItem[], direction: LayerMoveDirection) { + const { changed, nextLayers } = moveLayerOrderItemsByStep(layerList.value, items, direction) + if (!changed) { + return false + } + applyLayerList(nextLayers) + return true + } + + function canMoveLayerItemsToEdge(items: LayerOrderItem[], edge: LayerMoveEdge) { + return moveLayerOrderItemsToEdge(layerList.value, items, edge).changed + } + + function canMoveLayerItemsByStep(items: LayerOrderItem[], direction: LayerMoveDirection) { + return moveLayerOrderItemsByStep(layerList.value, items, direction).changed + } + + function canGroupLayers(layerIds: string[]) { + return canGroupLayersByIds(layerList.value, layerIds) + } + + function groupLayers(layerIds: string[], groupName?: string) { + const { changed, nextLayers, groupId } = groupLayersByIds(layerList.value, layerIds, groupName) + if (!changed) { + return null + } + applyLayerList(nextLayers) + return groupId ?? null + } + + function ungroupLayers(layerIds?: string[]) { + const { changed, nextLayers } = ungroupLayersByIds(layerList.value, layerIds) + if (!changed) { + return false + } + applyLayerList(nextLayers) + return true + } + + return { + toggleLayerLock, + toggleLayerVisible, + bringLayersToFront, + sendLayersToBack, + moveLayerBefore, + moveLayerItem, + moveLayerItemsToEdge, + moveLayerItemsByStep, + canMoveLayerItemsToEdge, + canMoveLayerItemsByStep, + canGroupLayers, + groupLayers, + ungroupLayers, + } +} + +export function useLayerSelection() { + const { selectedLayerId, selectedLayerIds, selectedGroup, layerList, canvasView } = useCanvasState() + const layerStore = useLayerStore() + const layerActions = useLayerActions() + + function setSelection(nextIds: string[], primaryId: string | null = nextIds[0] ?? null, options?: { groupId?: string | null }) { + // 主选中项和多选数组统一从这里收口,保证面板和舞台状态一致。 + layerStore.setSelectedLayerIds(nextIds, primaryId ?? '', options) + } + + function clearSelection() { + layerStore.clearLayerSelection() + } + + // 由外部图层面板触发,按组件 ID 精确选中 + function selectLayerById(id: string) { + const exists = layerList.value.some(item => item.id === id) + if (!exists) { + return false + } + setSelection([id], id) + return true + } + + // 由外部图层面板触发,多选组件并保持主选中项。 + function selectLayersByIds(ids: string[], primaryId?: string | null) { + if (!ids.length) { + clearSelection() + return false + } + const existingIds = [...new Set(ids)].filter(id => layerList.value.some(item => item.id === id)) + if (!existingIds.length) { + clearSelection() + return false + } + setSelection(existingIds, primaryId ?? existingIds[0] ?? null) + return true + } + + function selectGroupById(groupId: string, primaryId?: string | null) { + const group = getLayerGroupById(layerList.value, groupId) + if (!group) { + clearSelection() + return false + } + + setSelection(group.layerIds, primaryId ?? group.layerIds[0] ?? null, { groupId }) + return true + } + + async function removeSelectedLayers() { + if (!selectedLayerIds.value.length) { + return + } + // 批量删除(3个及以上)时弹出二次确认 + if (selectedLayerIds.value.length >= 3) { + try { + const { ElMessageBox } = await import('element-plus') + await ElMessageBox.confirm( + `确定删除选中的 ${selectedLayerIds.value.length} 个图层吗?此操作不可撤销。`, + '批量删除', + { confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' }, + ) + } + catch { + return + } + } + layerStore.removeLayersByIds(selectedLayerIds.value) + clearSelection() + } + + function getSelectedLayers() { + const fallbackIds = selectedLayerIds.value.length ? selectedLayerIds.value : selectedLayerId.value ? [selectedLayerId.value] : [] + const selectedSet = new Set(fallbackIds) + return layerList.value.filter(item => selectedSet.has(item.id)) + } + + function hasLockedSelectedLayers() { + return getSelectedLayers().some(layer => layer.config?.locked === true) + } + + function getSelectedOrderItems(): LayerOrderItem[] { + if (selectedGroup.value?.groupId) { + // 图层面板里选中分组时,排序相关操作需要把它看成单一条目。 + return [{ + kind: 'group', + id: `group:${selectedGroup.value.groupId}`, + groupId: selectedGroup.value.groupId, + } satisfies LayerOrderItem] + } + + const fallbackIds = selectedLayerIds.value.length ? selectedLayerIds.value : selectedLayerId.value ? [selectedLayerId.value] : [] + if (!fallbackIds.length) { + return [] + } + + const items: LayerOrderItem[] = [] + fallbackIds.forEach((id) => { + const layer = layerList.value.find(item => item.id === id) + if (!layer) { + return + } + items.push({ + kind: 'component', + id, + groupId: getLayerGroupMeta(layer)?.groupId ?? undefined, + }) + }) + return items + } + + function bringSelectedToFront() { + if (hasLockedSelectedLayers()) { + return + } + const orderItems = getSelectedOrderItems() + if (!orderItems.length) { + return + } + layerActions.moveLayerItemsToEdge(orderItems, 'front') + } + + function sendSelectedToBack() { + if (hasLockedSelectedLayers()) { + return + } + const orderItems = getSelectedOrderItems() + if (!orderItems.length) { + return + } + layerActions.moveLayerItemsToEdge(orderItems, 'back') + } + + function moveSelectedForward() { + if (hasLockedSelectedLayers()) { + return + } + const orderItems = getSelectedOrderItems() + if (!orderItems.length) { + return + } + layerActions.moveLayerItemsByStep(orderItems, 'forward') + } + + function moveSelectedBackward() { + if (hasLockedSelectedLayers()) { + return + } + const orderItems = getSelectedOrderItems() + if (!orderItems.length) { + return + } + layerActions.moveLayerItemsByStep(orderItems, 'backward') + } + + function canBringSelectedToFront() { + if (hasLockedSelectedLayers()) { + return false + } + const orderItems = getSelectedOrderItems() + return orderItems.length > 0 && layerActions.canMoveLayerItemsToEdge(orderItems, 'front') + } + + function canSendSelectedToBack() { + if (hasLockedSelectedLayers()) { + return false + } + const orderItems = getSelectedOrderItems() + return orderItems.length > 0 && layerActions.canMoveLayerItemsToEdge(orderItems, 'back') + } + + function canMoveSelectedForward() { + if (hasLockedSelectedLayers()) { + return false + } + const orderItems = getSelectedOrderItems() + return orderItems.length > 0 && layerActions.canMoveLayerItemsByStep(orderItems, 'forward') + } + + function canMoveSelectedBackward() { + if (hasLockedSelectedLayers()) { + return false + } + const orderItems = getSelectedOrderItems() + return orderItems.length > 0 && layerActions.canMoveLayerItemsByStep(orderItems, 'backward') + } + + function groupSelected(groupName?: string) { + if (!layerActions.canGroupLayers(selectedLayerIds.value)) { + return false + } + const groupId = layerActions.groupLayers(selectedLayerIds.value, groupName) + if (!groupId) { + return false + } + selectGroupById(groupId) + return true + } + + async function ungroupSelected() { + const nextIds = selectedGroup.value?.layerIds ?? [...selectedLayerIds.value] + // 组内图层较多时弹出二次确认 + if (nextIds.length >= 5) { + try { + const { ElMessageBox } = await import('element-plus') + await ElMessageBox.confirm( + `当前分组包含 ${nextIds.length} 个图层,确定解组吗?`, + '解组确认', + { confirmButtonText: '解组', cancelButtonText: '取消', type: 'info' }, + ) + } + catch { + return false + } + } + const ungrouped = layerActions.ungroupLayers(nextIds) + if (!ungrouped) { + return false + } + setSelection(nextIds, nextIds[0] ?? null, { groupId: null }) + return true + } + + function replaceLayers(nextLayers: Layer[]) { + layerStore.replaceLayers(nextLayers) + } + + function getDefaultItemSize(type = 'custom') { + const sizeMap: Record = { + rect: { width: 160, height: 100 }, + number: { width: 180, height: 72 }, + text: { width: 180, height: 64 }, + bar: { width: 48, height: 120 }, + button: { width: 120, height: 40 }, + pidController: { width: 160, height: 140 }, + valveController: { width: 100, height: 60 }, + canvasSwitcher: { width: 300, height: 40 }, + } + const preset = sizeMap[type] || { width: 140, height: 60 } + const baseWidth = preset.width + const baseHeight = preset.height + if (!canvasView.value.imageWidth || !canvasView.value.imageHeight) { + return { width: baseWidth, height: baseHeight } + } + return { + width: Math.min(baseWidth, canvasView.value.imageWidth), + height: Math.min(baseHeight, canvasView.value.imageHeight), + } + } + + function placeLayerAtClientPoint(pointer: PointerPosition, type = 'custom') { + if (!pointer || !canvasView.value.imageWidth || !canvasView.value.imageHeight) { + return false + } + if (pointer.canvasX < 0 || pointer.canvasY < 0 || pointer.canvasX > pointer.rect.width || pointer.canvasY > pointer.rect.height) { + return false + } + + const size = getDefaultItemSize(type) + const imageX = pointer.imageX + const imageY = pointer.imageY + + const clampedX = clamp(imageX, 0, Math.max(0, canvasView.value.imageWidth - size.width)) + const clampedY = clamp(imageY, 0, Math.max(0, canvasView.value.imageHeight - size.height)) + + const snappedX = clamp(snapToGrid(clampedX, GRID_SIZE), 0, Math.max(0, canvasView.value.imageWidth - size.width)) + const snappedY = clamp(snapToGrid(clampedY, GRID_SIZE), 0, Math.max(0, canvasView.value.imageHeight - size.height)) + + // 新增组件默认即为可见、可编辑状态,便于拖入后马上调整。 + const newLayer: Layer = { + id: createLayerId(), + type: type as any, + x: snappedX, + y: snappedY, + width: size.width, + height: size.height, + config: { + locked: false, + visible: true, + fillColor: type === 'button' ? '#C8C8C8' : type === 'rect' ? '#B7B7B7' : undefined, + radius: type === 'rect' ? 0 : undefined, + fontSize: type === 'number' ? 24 : type === 'text' ? 18 : undefined, + fontWeight: type === 'number' ? 700 : 500, + content: type === 'text' ? '文本' : undefined, + decimals: type === 'number' ? 2 : undefined, + ...(type === 'bar' + ? { + value: 50, + direction: 'vertical', + min: 0, + max: 100, + showValue: true, + fillColor: '#000000', + foregroundColor: '#00ff00', + } + : {}), + ...(type === 'button' + ? { + label: '按钮', + buttonType: 'trigger', + confirmRequired: false, + } + : {}), + ...(type === 'pidController' + ? { + headerColor: '#0055ff', + labelColor: '#00cc00', + valueColor: '#ffffff', + bgColor: '#1a1a2e', + } + : {}), + ...(type === 'valveController' + ? { + bgColor: '#1a1a2e', + textColor: '#ffffff', + } + : {}), + ...(type === 'canvasSwitcher' + ? { + items: [], + activeColor: '#1677ff', + inactiveColor: '#f0f0f0', + activeTextColor: '#ffffff', + inactiveTextColor: '#333333', + } + : {}), + }, + } + + layerStore.appendLayers([newLayer]) + setSelection([newLayer.id], newLayer.id) + return true + } + + function placeTemplateAtPoint(pointer: PointerPosition, templateLayers: Layer[]) { + if (!pointer || !templateLayers.length || !canvasView.value.imageWidth || !canvasView.value.imageHeight) { + return false + } + if (pointer.canvasX < 0 || pointer.canvasY < 0 || pointer.canvasX > pointer.rect.width || pointer.canvasY > pointer.rect.height) { + return false + } + + // 计算模板图层的包围盒 + const minX = Math.min(...templateLayers.map(l => l.x)) + const minY = Math.min(...templateLayers.map(l => l.y)) + + // 将模板放到拖放位置,保持相对位置 + const offsetX = pointer.imageX - minX + const offsetY = pointer.imageY - minY + + const newLayers = templateLayers.map(l => ({ + ...JSON.parse(JSON.stringify(l)), + id: createLayerId(), + x: clamp(l.x + offsetX, 0, canvasView.value.imageWidth - l.width), + y: clamp(l.y + offsetY, 0, canvasView.value.imageHeight - l.height), + })) + + layerStore.appendLayers(newLayers) + setSelection(newLayers.map(l => l.id), newLayers[0].id) + return true + } + + return { + setSelection, + clearSelection, + selectLayerById, + selectLayersByIds, + selectGroupById, + removeSelectedLayers, + getSelectedLayers, + bringSelectedToFront, + sendSelectedToBack, + moveSelectedForward, + moveSelectedBackward, + canBringSelectedToFront, + canSendSelectedToBack, + canMoveSelectedForward, + canMoveSelectedBackward, + canGroupSelected: () => layerActions.canGroupLayers(selectedLayerIds.value), + groupSelected, + ungroupSelected, + replaceLayers, + placeLayerAtClientPoint, + placeTemplateAtPoint, + } +} diff --git a/packages/core/src/components/editor/canvas/composables/page-actions.ts b/packages/core/src/components/editor/canvas/composables/page-actions.ts new file mode 100644 index 0000000..0c1998a --- /dev/null +++ b/packages/core/src/components/editor/canvas/composables/page-actions.ts @@ -0,0 +1,38 @@ +import { storeToRefs } from 'pinia' +import { deleteCanvasApi } from '@/api' +import pinia, { useCanvasStore } from '@/stores' + +type DeletePageResult + = | { ok: true } + | { ok: false, reason: 'not_found' | 'last_page' } + +export function useCanvasPageActions() { + const canvasStore = useCanvasStore(pinia) + const { canvasList, canvasId } = storeToRefs(canvasStore) + + async function deletePageById(pageId: string): Promise { + const exists = canvasList.value.some(page => page.id === pageId) + if (!exists) { + return { ok: false, reason: 'not_found' } + } + + if (canvasList.value.length <= 1) { + return { ok: false, reason: 'last_page' } + } + + await deleteCanvasApi(pageId) + const nextList = await canvasStore.getCanvasList() + + if (canvasId.value === pageId) { + // 当前页被删掉时自动切到新列表中的第一页,避免编辑器落在空状态。 + const [nextCanvas] = nextList + canvasStore.setCanvasId(nextCanvas?.id || null) + } + + return { ok: true } + } + + return { + deletePageById, + } +} diff --git a/packages/core/src/components/editor/canvas/composables/persistence.ts b/packages/core/src/components/editor/canvas/composables/persistence.ts new file mode 100644 index 0000000..0931458 --- /dev/null +++ b/packages/core/src/components/editor/canvas/composables/persistence.ts @@ -0,0 +1,112 @@ +import { useDebounceFn } from '@vueuse/core' +import { ref } from 'vue' +import { updateCanvasBaseLayer, updateComponentsLayer } from '@/api' +import { useCanvasState } from '../context/state' + +// Module-level shared state for save status +const saveStatus = ref<'saved' | 'saving' | 'idle' | 'error'>('idle') + +const MAX_RETRIES = 3 +const RETRY_BASE_DELAY = 500 + +async function withRetry(fn: () => Promise, retries = MAX_RETRIES): Promise { + for (let attempt = 0; attempt <= retries; attempt++) { + try { + return await fn() + } + catch (error) { + if (attempt === retries) + throw error + await new Promise(resolve => setTimeout(resolve, RETRY_BASE_DELAY * (attempt + 1))) + } + } + throw new Error('unreachable') +} + +/** External components (e.g., StatusBar) can read save status via this */ +export function useSaveStatus() { + return { saveStatus } +} + +async function notifySaveError(context: string, error: unknown) { + try { + const { ElNotification } = await import('element-plus') + ElNotification.error({ + title: '保存失败', + message: `${context}保存失败,请检查存储空间或刷新页面重试。`, + duration: 5000, + }) + } + catch { + // 静默:通知组件不可用时不影响主流程 + } + console.error(`[canvas] ${context} 保存失败`, error) +} + +// 组件图层数据持久化相关逻辑 +export function useLayerPersistence() { + const { canvasId, layerList, isHydratingCanvas, canvasView, activeCanvasThumbnail } = useCanvasState() + + async function persistComponentsLayerNow() { + // 画布恢复阶段跳过保存,避免把服务端数据刚拉下来又原样写回。 + if (!canvasId.value || isHydratingCanvas.value) { + return + } + + const id = canvasId.value + saveStatus.value = 'saving' + try { + await withRetry(() => updateComponentsLayer({ + id, + components: layerList.value, + })) + saveStatus.value = 'saved' + } + catch (error) { + saveStatus.value = 'error' + notifySaveError('组件图层', error) + } + } + + // 组件变更统一走防抖保存,减少拖拽/缩放期间的高频请求。 + const persistComponentsLayer = useDebounceFn(persistComponentsLayerNow, 300) + + // 底图属性(宽高/宽高比锁定)自动保存 + async function persistCanvasBaseLayerNow() { + if (!canvasId.value || isHydratingCanvas.value) { + return + } + + if (!canvasView.value.imageWidth || !canvasView.value.imageHeight) { + return + } + + const id = canvasId.value + saveStatus.value = 'saving' + try { + await withRetry(() => updateCanvasBaseLayer({ + id, + width: Math.round(canvasView.value.imageWidth), + height: Math.round(canvasView.value.imageHeight), + thumbnail: activeCanvasThumbnail.value ? activeCanvasThumbnail.value : null, + lockAspectRatio: canvasView.value.backgroundLockAspectRatio, + backgroundColor: canvasView.value.background, + })) + saveStatus.value = 'saved' + } + catch (error) { + saveStatus.value = 'error' + notifySaveError('底图属性', error) + } + } + + // 底图属性也走独立的防抖保存,避免和组件数据互相影响。 + const persistCanvasBaseLayer = useDebounceFn(persistCanvasBaseLayerNow, 300) + + return { + saveStatus, + persistComponentsLayerNow, + persistComponentsLayer, + persistCanvasBaseLayer, + } +} diff --git a/packages/core/src/components/editor/canvas/composables/useHistory.ts b/packages/core/src/components/editor/canvas/composables/useHistory.ts new file mode 100644 index 0000000..edc9ae3 --- /dev/null +++ b/packages/core/src/components/editor/canvas/composables/useHistory.ts @@ -0,0 +1,161 @@ +import type { ComputedRef } from 'vue' +import type { CanvasView, Layer } from '../types' +import { computed, ref, shallowRef } from 'vue' + +export interface BaseLayerSnapshot { + imageWidth: number + imageHeight: number + background: string + backgroundLockAspectRatio: boolean +} + +export interface HistoryEntry { + layerList: Layer[] + baseLayer: BaseLayerSnapshot + timestamp: number + label: string +} + +export interface UseHistoryReturn { + canUndo: ComputedRef + canRedo: ComputedRef + undoLabel: ComputedRef + redoLabel: ComputedRef + pushState: (label: string, mergeContext?: { layerIds?: string[], fieldPath?: string }) => void + undo: () => void + redo: () => void + clear: () => void + isRestoring: ComputedRef +} + +const MAX_HISTORY = 50 +const MERGE_WINDOW_MS = 300 + +interface MergeKey { + label: string + layerIds?: string[] + fieldPath?: string +} + +function isSameMergeKey(a: MergeKey, b: MergeKey): boolean { + if (a.label !== b.label) + return false + if (a.fieldPath !== b.fieldPath) + return false + const aIds = a.layerIds?.join(',') ?? '' + const bIds = b.layerIds?.join(',') ?? '' + return aIds === bIds +} + +export function useHistory( + getLayerList: () => Layer[], + getCanvasView: () => CanvasView, + onRestore: (entry: HistoryEntry) => void, +): UseHistoryReturn { + const undoStack = shallowRef([]) + const redoStack = shallowRef([]) + const _isRestoring = ref(false) + const lastMergeKey = ref({ label: '' }) + const lastPushTime = ref(0) + + function captureSnapshot(label: string): HistoryEntry { + const view = getCanvasView() + return { + layerList: structuredClone(getLayerList()), + baseLayer: { + imageWidth: view.imageWidth, + imageHeight: view.imageHeight, + background: view.background, + backgroundLockAspectRatio: view.backgroundLockAspectRatio, + }, + timestamp: Date.now(), + label, + } + } + + function pushState(label: string, mergeContext?: { layerIds?: string[], fieldPath?: string }) { + if (_isRestoring.value) + return + const now = Date.now() + const currentKey: MergeKey = { label, layerIds: mergeContext?.layerIds, fieldPath: mergeContext?.fieldPath } + if ( + isSameMergeKey(lastMergeKey.value, currentKey) + && now - lastPushTime.value < MERGE_WINDOW_MS + && undoStack.value.length > 0 + ) { + lastPushTime.value = now + return + } + const entry = captureSnapshot(label) + const next = [...undoStack.value, entry] + if (next.length > MAX_HISTORY) { + next.splice(0, next.length - MAX_HISTORY) + } + undoStack.value = next + redoStack.value = [] + lastMergeKey.value = currentKey + lastPushTime.value = now + } + + function undo() { + if (undoStack.value.length <= 1) + return + _isRestoring.value = true + try { + const stack = [...undoStack.value] + const current = stack.pop()! + redoStack.value = [...redoStack.value, current] + undoStack.value = stack + const target = stack.at(-1) + if (target) { + onRestore(target) + } + } + finally { + _isRestoring.value = false + } + lastMergeKey.value = { label: '' } + lastPushTime.value = 0 + } + + function redo() { + if (redoStack.value.length === 0) + return + _isRestoring.value = true + try { + const stack = [...redoStack.value] + const target = stack.pop()! + redoStack.value = stack + undoStack.value = [...undoStack.value, target] + onRestore(target) + } + finally { + _isRestoring.value = false + } + lastMergeKey.value = { label: '' } + lastPushTime.value = 0 + } + + function clear() { + undoStack.value = [] + redoStack.value = [] + lastMergeKey.value = { label: '' } + lastPushTime.value = 0 + } + + const canUndo = computed(() => undoStack.value.length > 1) + const canRedo = computed(() => redoStack.value.length > 0) + const undoLabel = computed(() => { + if (undoStack.value.length <= 1) + return '' + return undoStack.value.at(-1)?.label || '' + }) + const redoLabel = computed(() => { + if (redoStack.value.length === 0) + return '' + return redoStack.value.at(-1)?.label || '' + }) + const isRestoring = computed(() => _isRestoring.value) + + return { canUndo, canRedo, undoLabel, redoLabel, pushState, undo, redo, clear, isRestoring } +} diff --git a/packages/core/src/components/editor/canvas/composables/useLayerAlignment.ts b/packages/core/src/components/editor/canvas/composables/useLayerAlignment.ts new file mode 100644 index 0000000..58920e0 --- /dev/null +++ b/packages/core/src/components/editor/canvas/composables/useLayerAlignment.ts @@ -0,0 +1,111 @@ +import type { LayerPositionUpdate } from './useLayerMutations' +import { useCanvasState } from '../context/state' +import { clamp, snapToGrid } from '../utils' + +const GRID_SIZE = 10 + +type AlignType = 'left' | 'center' | 'right' | 'top' | 'middle' | 'bottom' + +export function useLayerAlignment(deps: { + updateLayerPosition: (payload: LayerPositionUpdate) => void + updateLayerPositions: (updates: LayerPositionUpdate[]) => void +}) { + const { canvasView, layerList, selectedLayerId, selectedLayerIds } = useCanvasState() + + function getSelectedLayers() { + const fallbackIds = selectedLayerIds.value.length ? selectedLayerIds.value : selectedLayerId.value ? [selectedLayerId.value] : [] + const selectedSet = new Set(fallbackIds) + return layerList.value.filter(layer => selectedSet.has(layer.id)) + } + + function alignSelected(type: AlignType) { + if (!canvasView.value.imageWidth || !canvasView.value.imageHeight) { + return + } + + const layers = getSelectedLayers() + if (!layers.length) { + return + } + + if (layers.length === 1) { + // 单个图层时参考对象是整个底图,多图层时参考对象是选区包围盒。 + const layer = layers[0] + let newX = layer.x + let newY = layer.y + + switch (type) { + case 'left': + newX = 0 + break + case 'center': + newX = (canvasView.value.imageWidth - layer.width) / 2 + break + case 'right': + newX = canvasView.value.imageWidth - layer.width + break + case 'top': + newY = 0 + break + case 'middle': + newY = (canvasView.value.imageHeight - layer.height) / 2 + break + case 'bottom': + newY = canvasView.value.imageHeight - layer.height + break + } + + deps.updateLayerPosition({ + id: layer.id, + nextX: clamp(snapToGrid(newX, GRID_SIZE), 0, Math.max(0, canvasView.value.imageWidth - layer.width)), + nextY: clamp(snapToGrid(newY, GRID_SIZE), 0, Math.max(0, canvasView.value.imageHeight - layer.height)), + }) + return + } + + const left = Math.min(...layers.map(layer => layer.x)) + const top = Math.min(...layers.map(layer => layer.y)) + const right = Math.max(...layers.map(layer => layer.x + layer.width)) + const bottom = Math.max(...layers.map(layer => layer.y + layer.height)) + const centerX = (left + right) / 2 + const centerY = (top + bottom) / 2 + + const updates: LayerPositionUpdate[] = layers.map((layer) => { + let nextX = layer.x + let nextY = layer.y + + switch (type) { + case 'left': + nextX = left + break + case 'center': + nextX = centerX - layer.width / 2 + break + case 'right': + nextX = right - layer.width + break + case 'top': + nextY = top + break + case 'middle': + nextY = centerY - layer.height / 2 + break + case 'bottom': + nextY = bottom - layer.height + break + } + + return { + id: layer.id, + nextX: clamp(snapToGrid(nextX, GRID_SIZE), 0, Math.max(0, canvasView.value.imageWidth - layer.width)), + nextY: clamp(snapToGrid(nextY, GRID_SIZE), 0, Math.max(0, canvasView.value.imageHeight - layer.height)), + } + }) + + deps.updateLayerPositions(updates) + } + + return { + alignSelected, + } +} diff --git a/packages/core/src/components/editor/canvas/composables/useLayerController.ts b/packages/core/src/components/editor/canvas/composables/useLayerController.ts new file mode 100644 index 0000000..f07225f --- /dev/null +++ b/packages/core/src/components/editor/canvas/composables/useLayerController.ts @@ -0,0 +1,227 @@ +import { computed, watch } from 'vue' +import { useComponentTemplates } from '@/composables/useComponentTemplates' +import { useOperationLog } from '@/composables/useOperationLog' +import { useCanvasInject } from '../context' +import { useCanvasState } from '../context/state' +import { listenEditorEmitter, setEditorEmitter } from '../emitter' +import { useLayerActions, useLayerSelection } from './layer' +import { useLayerPersistence } from './persistence' +import { useHistory } from './useHistory' +import { useLayerAlignment } from './useLayerAlignment' +import { useLayerMutations } from './useLayerMutations' +import { useLayerSelectionSync } from './useLayerSelectionSync' +import { useSelectionBox } from './useSelectionBox' + +export function useLayerController() { + const { layerList, selectedLayerId, selectedLayerIds, isHydratingCanvas, canvasView } = useCanvasState() + const { log: logOperation } = useOperationLog() + const { + clearSelection, + setSelection, + selectLayerById, + selectLayersByIds, + bringSelectedToFront: _bringSelectedToFront, + sendSelectedToBack: _sendSelectedToBack, + groupSelected: _groupSelected, + ungroupSelected: _ungroupSelected, + placeLayerAtClientPoint, + placeTemplateAtPoint, + replaceLayers, + } = useLayerSelection() + const { toggleLayerLock, toggleLayerVisible } = useLayerActions() + const { + updateLayerPosition, + updateLayerPositions, + updateLayerRect, + updateLayerRects, + updateLayerField, + updateLayerFields, + } = useLayerMutations() + const { onBackgroundPointerDown } = useSelectionBox() + const { alignSelected } = useLayerAlignment({ + updateLayerPosition, + updateLayerPositions, + }) + + useLayerSelectionSync() + + const selectedCount = computed(() => selectedLayerIds.value.length) + + const { persistComponentsLayer, persistCanvasBaseLayer } = useLayerPersistence() + watch( + layerList, + () => { + // 组件拖拽、缩放、删除等变更后自动落盘。 + persistComponentsLayer() + }, + { deep: true }, + ) + + const history = useHistory( + () => layerList.value, + () => canvasView.value, + (entry) => { + // 恢复图层 + replaceLayers(entry.layerList) + // 恢复底图属性 + const cv = canvasView.value + cv.imageWidth = entry.baseLayer.imageWidth + cv.imageHeight = entry.baseLayer.imageHeight + cv.background = entry.baseLayer.background + cv.backgroundLockAspectRatio = entry.baseLayer.backgroundLockAspectRatio + persistCanvasBaseLayer() + }, + ) + + const { getPointerImagePosition } = useCanvasInject() + const { getTemplate } = useComponentTemplates() + + function onCanvasDrop({ event, type }: { event: DragEvent, type: string }) { + // 模板拖入 + if (type === '__template__') { + let templateId = '' + try { + const data = JSON.parse(event.dataTransfer?.getData('application/json') ?? '{}') + templateId = data.templateId ?? '' + } + catch { /* ignore */ } + + const template = templateId ? getTemplate(templateId) : null + if (template) { + const pointer = getPointerImagePosition(event.clientX, event.clientY) + const placed = placeTemplateAtPoint(pointer, template.layers) + if (placed) { + history.pushState('从模板添加') + logOperation('create', template.name, '从模板添加') + } + } + return + } + + const pointer = getPointerImagePosition(event.clientX, event.clientY) + const placed = placeLayerAtClientPoint(pointer, type) + if (placed) { + history.pushState('添加图层') + logOperation('create', type, '添加图层') + } + } + listenEditorEmitter('canvasStage:drop', onCanvasDrop) + + function onLayerClick(payload: { event: MouseEvent, id: string }) { + const { event, id } = payload + const additive = event.metaKey || event.ctrlKey || event.shiftKey + + if (!additive) { + setSelection([id], id, { groupId: null }) + return + } + + const exists = selectedLayerIds.value.includes(id) + if (exists) { + // 加选模式下再次点击已选图层,等价于把它从当前选区移除。 + const next = selectedLayerIds.value.filter(entry => entry !== id) + setSelection(next, selectedLayerId.value === id ? next[0] ?? null : selectedLayerId.value) + return + } + + setSelection([...selectedLayerIds.value, id], id) + } + + function onCanvasLayerClick(payload: { event: MouseEvent, id: string }) { + onLayerClick(payload) + } + listenEditorEmitter('canvasStage:layerClick', onCanvasLayerClick) + + function toggleLayerLockState(id: string, locked?: boolean) { + toggleLayerLock(id, locked) + } + + function toggleLayerVisibleState(id: string, visible?: boolean) { + toggleLayerVisible(id, visible) + } + + watch(isHydratingCanvas, (hydrating, prevHydrating) => { + if (prevHydrating && !hydrating) { + history.pushState('初始状态') + } + }) + + // 监听来自 header 按钮和键盘快捷键的撤销/重做事件 + listenEditorEmitter('header:undo', () => { + history.undo() + logOperation('undo', '', '撤销操作') + }) + listenEditorEmitter('header:redo', () => { + history.redo() + logOperation('redo', '', '重做操作') + }) + listenEditorEmitter('history:push', ({ label }) => { + history.pushState(label) + logOperation('edit', '', label) + }) + + // 向 header 广播历史状态变化 + watch([history.canUndo, history.canRedo, history.undoLabel, history.redoLabel], () => { + setEditorEmitter('history:stateChange', { + canUndo: history.canUndo.value, + canRedo: history.canRedo.value, + undoLabel: history.undoLabel.value, + redoLabel: history.redoLabel.value, + }) + }) + + // 包装图层操作,在操作成功后自动记录历史 + function bringSelectedToFront() { + _bringSelectedToFront() + history.pushState('调整层级') + logOperation('edit', selectedLayerIds.value.join(','), '前置图层') + } + function sendSelectedToBack() { + _sendSelectedToBack() + history.pushState('调整层级') + logOperation('edit', selectedLayerIds.value.join(','), '后置图层') + } + function groupSelected(groupName?: string) { + const result = _groupSelected(groupName) + if (result) { + history.pushState('创建分组') + logOperation('group', groupName ?? '', '创建分组') + } + return result + } + async function ungroupSelected() { + const result = await _ungroupSelected() + if (result) { + history.pushState('解除分组') + logOperation('group', '', '解除分组') + } + return result + } + + return { + layerList, + selectedLayerIds, + selectedLayerId, + selectedCount, + onCanvasDrop, + onBackgroundPointerDown, + onLayerClick, + selectLayerById, + selectLayersByIds, + clearSelection, + updateLayerPosition, + updateLayerPositions, + updateLayerRect, + updateLayerRects, + alignSelected, + bringSelectedToFront, + sendSelectedToBack, + toggleLayerLockState, + toggleLayerVisibleState, + updateLayerField, + updateLayerFields, + groupSelected, + ungroupSelected, + history, + } +} diff --git a/packages/core/src/components/editor/canvas/composables/useLayerMutations.ts b/packages/core/src/components/editor/canvas/composables/useLayerMutations.ts new file mode 100644 index 0000000..699dfaf --- /dev/null +++ b/packages/core/src/components/editor/canvas/composables/useLayerMutations.ts @@ -0,0 +1,63 @@ +import pinia, { useLayerStore } from '@/stores' +import { listenEditorEmitter } from '../emitter' + +export interface LayerPositionUpdate { + id: string + nextX: number + nextY: number +} + +export interface LayerRectUpdate extends LayerPositionUpdate { + nextWidth: number + nextHeight: number + options?: { ignoreLock?: boolean } +} + +export function useLayerMutations() { + const layerStore = useLayerStore(pinia) + + function updateLayerPosition(payload: LayerPositionUpdate) { + layerStore.updateLayerPosition(payload) + } + + function updateLayerPositions(updates: LayerPositionUpdate[]) { + layerStore.updateLayerPositions(updates) + } + + function updateLayerRect(payload: LayerRectUpdate) { + layerStore.updateLayerRect(payload) + } + + function updateLayerRects(updates: LayerRectUpdate[]) { + layerStore.updateLayerRects(updates) + } + + function updateLayerField(id: string, field: string, value: unknown, options?: { ignoreLock?: boolean }) { + layerStore.updateLayerField(id, field, value, options) + } + + function updateLayerFields(ids: string[], field: string, value: unknown, options?: { ignoreLock?: boolean }) { + layerStore.updateLayerFields(ids, field, value, options) + } + + // 将舞台、图层面板、属性面板的更新请求统一桥接到 layer store。 + listenEditorEmitter('layer:updatePosition', updateLayerPosition) + listenEditorEmitter('layer:updatePositions', updateLayerPositions) + listenEditorEmitter('layer:updateRect', updateLayerRect) + listenEditorEmitter('layer:updateRects', updateLayerRects) + listenEditorEmitter('layer:updateField', ({ id, field, value, options }) => { + updateLayerField(id, field, value, options) + }) + listenEditorEmitter('layer:updateFields', ({ ids, field, value, options }) => { + updateLayerFields(ids, field, value, options) + }) + + return { + updateLayerPosition, + updateLayerPositions, + updateLayerRect, + updateLayerRects, + updateLayerField, + updateLayerFields, + } +} diff --git a/packages/core/src/components/editor/canvas/composables/useLayerRender.ts b/packages/core/src/components/editor/canvas/composables/useLayerRender.ts new file mode 100644 index 0000000..8e10233 --- /dev/null +++ b/packages/core/src/components/editor/canvas/composables/useLayerRender.ts @@ -0,0 +1,29 @@ +import type { CSSProperties } from 'vue' +import type { Layer } from '../types' +import { useCanvasState } from '../context/state' + +export function useLayerRender() { + const { canvasView } = useCanvasState() + + function getLayerStyle(layer: Layer): CSSProperties { + // 图层 DOM 始终按当前视口偏移和缩放换算,和底图保持同一坐标系。 + const left = canvasView.value.offsetX + layer.x * canvasView.value.scale + const top = canvasView.value.offsetY + layer.y * canvasView.value.scale + const width = layer.width * canvasView.value.scale + const height = layer.height * canvasView.value.scale + const rotate = layer.config?.rotate ?? 0 + return { + left: `${left}px`, + top: `${top}px`, + width: `${width}px`, + height: `${height}px`, + opacity: layer.config?.visible === false ? 0.35 : 1, + cursor: layer.config?.locked ? 'not-allowed' : 'pointer', + ...(rotate ? { transform: `rotate(${rotate}deg)`, transformOrigin: 'center center' } : {}), + } + } + + return { + getLayerStyle, + } +} diff --git a/packages/core/src/components/editor/canvas/composables/useLayerSelectionSync.ts b/packages/core/src/components/editor/canvas/composables/useLayerSelectionSync.ts new file mode 100644 index 0000000..bba6ce5 --- /dev/null +++ b/packages/core/src/components/editor/canvas/composables/useLayerSelectionSync.ts @@ -0,0 +1,122 @@ +import { watch } from 'vue' +import pinia, { useLayerStore } from '@/stores' +import { useCanvasState } from '../context/state' +import { setEditorEmitter } from '../emitter' +import { useLayerSelection } from './layer' + +export function useLayerSelectionSync() { + const { layerList, selectedLayerId, selectedLayerIds, selectedGroupId, isBaseLayerActive } = useCanvasState() + const layerStore = useLayerStore(pinia) + const { selectLayerById } = useLayerSelection() + + watch( + () => selectedLayerIds.value, + (ids) => { + // 多选数组是主要真值,主选中项和属性面板展示都从它推导。 + if (!ids.length) { + layerStore.setLayerId('') + if (isBaseLayerActive.value) { + setEditorEmitter('propertyPanel:openPanel') + } + else { + setEditorEmitter('propertyPanel:closePanel') + } + return + } + + const primaryId = ids.includes(selectedLayerId.value) ? selectedLayerId.value : ids.at(-1) + if (primaryId && primaryId !== selectedLayerId.value) { + layerStore.setLayerId(primaryId) + return + } + if (selectedGroupId.value) { + setEditorEmitter('propertyPanel:openPanel') + return + } + if (ids.length === 1) { + setEditorEmitter('propertyPanel:openPanel') + return + } + setEditorEmitter('propertyPanel:closePanel') + }, + { deep: false, flush: 'sync' }, + ) + + watch( + () => selectedLayerId.value, + (nextLayerId) => { + // 外部如果只改了主选中项,这里会把多选数组补齐回一致状态。 + if (!nextLayerId) { + return + } + if (selectedGroupId.value && selectedLayerIds.value.includes(nextLayerId)) { + setEditorEmitter('propertyPanel:openPanel') + return + } + if (selectedLayerIds.value.length > 1 && selectedLayerIds.value.includes(nextLayerId)) { + setEditorEmitter('propertyPanel:closePanel') + return + } + const selected = selectLayerById(nextLayerId) + if (selected) { + setEditorEmitter('propertyPanel:openPanel') + return + } + layerStore.setLayerId('') + }, + { flush: 'sync' }, + ) + + watch( + () => isBaseLayerActive.value, + (active) => { + if (selectedLayerIds.value.length) { + return + } + if (active) { + setEditorEmitter('propertyPanel:openPanel') + } + else { + setEditorEmitter('propertyPanel:closePanel') + } + }, + { flush: 'sync' }, + ) + + watch( + () => selectedGroupId.value, + (groupId) => { + if (groupId) { + setEditorEmitter('propertyPanel:openPanel') + return + } + if (selectedLayerIds.value.length > 1) { + setEditorEmitter('propertyPanel:closePanel') + } + }, + { flush: 'sync' }, + ) + + watch( + layerList, + () => { + // 图层被删除后兜底清空失效选中态,避免属性面板指向幽灵节点。 + if (!selectedLayerId.value) { + return + } + const exists = layerList.value.some(item => item.id === selectedLayerId.value) + if (!exists) { + layerStore.setLayerId('') + if (!selectedLayerIds.value.length) { + if (isBaseLayerActive.value) { + setEditorEmitter('propertyPanel:openPanel') + } + else { + setEditorEmitter('propertyPanel:closePanel') + } + } + } + }, + { deep: true }, + ) +} diff --git a/packages/core/src/components/editor/canvas/composables/useSelectionBox.ts b/packages/core/src/components/editor/canvas/composables/useSelectionBox.ts new file mode 100644 index 0000000..7a97515 --- /dev/null +++ b/packages/core/src/components/editor/canvas/composables/useSelectionBox.ts @@ -0,0 +1,152 @@ +import { onBeforeUnmount } from 'vue' +import { useCanvasInject } from '../context' +import { useCanvasState } from '../context/state' +import { listenEditorEmitter, setEditorEmitter } from '../emitter' +import { buildLayerGroups } from '../grouping' +import { normalizeRect, rectIntersects } from '../utils' +import { useLayerSelection } from './layer' + +const BOX_SELECT_THRESHOLD = 4 + +export function useSelectionBox() { + const { canvasView, layerList, selectedLayerId, selectedLayerIds, isBaseLayerActive, boxSelect } = useCanvasState() + const { clearSelection, selectGroupById, setSelection } = useLayerSelection() + const { getPointerImagePosition } = useCanvasInject() + const pointerTracker = { + startClientX: 0, + startClientY: 0, + currentClientX: 0, + currentClientY: 0, + hitGroupId: '', + panelSuspended: false, + } + + function isPointInsideRect(point: { imageX: number, imageY: number }, rect: { x: number, y: number, width: number, height: number }) { + return point.imageX >= rect.x + && point.imageY >= rect.y + && point.imageX <= rect.x + rect.width + && point.imageY <= rect.y + rect.height + } + + function isPointMoved() { + return Math.abs(pointerTracker.currentClientX - pointerTracker.startClientX) > BOX_SELECT_THRESHOLD + || Math.abs(pointerTracker.currentClientY - pointerTracker.startClientY) > BOX_SELECT_THRESHOLD + } + + function stopBoxSelect() { + // 统一从这里收尾,确保事件和属性面板状态都能被正确恢复。 + boxSelect.value.active = false + boxSelect.value.moved = false + pointerTracker.hitGroupId = '' + if (pointerTracker.panelSuspended) { + pointerTracker.panelSuspended = false + setEditorEmitter('propertyPanel:resume') + } + window.removeEventListener('pointermove', onBoxSelectMove) + window.removeEventListener('pointerup', onBoxSelectUp) + } + + function onBoxSelectMove(event: PointerEvent) { + if (!boxSelect.value.active) { + return + } + const pointer = getPointerImagePosition(event.clientX, event.clientY) + if (!pointer) { + return + } + pointerTracker.currentClientX = event.clientX + pointerTracker.currentClientY = event.clientY + boxSelect.value.currentX = pointer.imageX + boxSelect.value.currentY = pointer.imageY + boxSelect.value.moved = isPointMoved() + if (boxSelect.value.moved && !pointerTracker.panelSuspended) { + pointerTracker.panelSuspended = true + setEditorEmitter('propertyPanel:suspend') + } + } + + function onBoxSelectUp() { + if (!boxSelect.value.active) { + return + } + + if (!boxSelect.value.moved) { + if (pointerTracker.hitGroupId) { + selectGroupById(pointerTracker.hitGroupId) + stopBoxSelect() + return + } + + if (!boxSelect.value.additive && !isBaseLayerActive.value) { + clearSelection() + } + stopBoxSelect() + return + } + + const selectionRect = normalizeRect(boxSelect.value.startX, boxSelect.value.startY, boxSelect.value.currentX, boxSelect.value.currentY) + // 框选采用“矩形相交”策略,而不是完全包裹,降低选中门槛。 + const nextIds = layerList.value + .filter(item => rectIntersects(selectionRect, item)) + .map(item => item.id) + + if (boxSelect.value.additive) { + setSelection([...selectedLayerIds.value, ...nextIds], nextIds[0] ?? selectedLayerId.value) + } + else { + setSelection(nextIds, nextIds[0] ?? null) + } + + stopBoxSelect() + } + + function onBackgroundPointerDown(event: PointerEvent) { + if (event.button !== 0) { + return false + } + const pointer = getPointerImagePosition(event.clientX, event.clientY) + if (!pointer) { + return false + } + if ( + pointer.imageX < 0 + || pointer.imageY < 0 + || pointer.imageX > canvasView.value.imageWidth + || pointer.imageY > canvasView.value.imageHeight + ) { + return false + } + + event.preventDefault() + boxSelect.value.active = true + boxSelect.value.additive = event.metaKey || event.ctrlKey || event.shiftKey + boxSelect.value.moved = false + boxSelect.value.startX = pointer.imageX + boxSelect.value.startY = pointer.imageY + boxSelect.value.currentX = pointer.imageX + boxSelect.value.currentY = pointer.imageY + pointerTracker.startClientX = event.clientX + pointerTracker.startClientY = event.clientY + pointerTracker.currentClientX = event.clientX + pointerTracker.currentClientY = event.clientY + // 点按时顺手命中分组,允许“单击组选中,拖拽时框选”的双重语义。 + const hitGroup = buildLayerGroups(layerList.value) + .find(group => isPointInsideRect(pointer, group)) + pointerTracker.hitGroupId = hitGroup?.groupId ?? '' + + window.addEventListener('pointermove', onBoxSelectMove) + window.addEventListener('pointerup', onBoxSelectUp) + return true + } + + listenEditorEmitter('selection:backgroundPointerDown', onBackgroundPointerDown) + + onBeforeUnmount(() => { + stopBoxSelect() + }) + + return { + onBackgroundPointerDown, + stopBoxSelect, + } +} diff --git a/packages/core/src/components/editor/canvas/context/canvas.ts b/packages/core/src/components/editor/canvas/context/canvas.ts new file mode 100644 index 0000000..a6bb1ab --- /dev/null +++ b/packages/core/src/components/editor/canvas/context/canvas.ts @@ -0,0 +1,78 @@ +import type { CanvasStageExpose } from '../types' +import { inject, provide, shallowRef } from 'vue' +import { unwrapElement } from '../utils' +import { useCanvasState } from './state' + +export type CanvasCursorMode = 'default' | 'move' + +export type PointerPosition = { + imageX: number + imageY: number + rect: DOMRect + canvasX: number + canvasY: number +} | null + +const CanvasProviderKey = Symbol('canvas-provider') + +export function useCanvasProvider() { + // 编辑器主区域的 ref,包含画布和背景等元素。用于事件监听时判断事件是否发生在编辑器主区域。 + const editorRef = shallowRef(null) + + // 画布 ref,包含画布元素和相关方法。用于在属性面板等子组件中访问和操作画布。 + const stageRef = shallowRef(null) + + function getWrapperElement() { + return unwrapElement(stageRef.value?.wrapperRef) + } + + function getCanvasElement() { + return unwrapElement(stageRef.value?.canvasRef) as HTMLCanvasElement + } + + const { canvasView } = useCanvasState() + + function getPointerImagePosition(clientX: number, clientY: number): PointerPosition { + const canvas = getCanvasElement() + if (!canvas || canvasView.value.scale <= 0) { + return null + } + const rect = canvas.getBoundingClientRect() + const canvasX = clientX - rect.left + const canvasY = clientY - rect.top + + // 同时返回屏幕坐标和底图坐标,供拖拽放置、框选、缩放复用。 + return { + imageX: (canvasX - canvasView.value.offsetX) / canvasView.value.scale, + imageY: (canvasY - canvasView.value.offsetY) / canvasView.value.scale, + rect, + canvasX, + canvasY, + } + } + + // 属性面板 ref,包含属性面板元素和相关方法。用于在画布等父组件中访问和操作属性面板。 + const propertyPanelRef = shallowRef(null) + + // 左侧边栏 ref,包含图层、组件等编辑器侧边面板。 + const sidebarRef = shallowRef(null) + + const state = { + editorRef, + stageRef, + propertyPanelRef, + sidebarRef, + getWrapperElement, + getCanvasElement, + getPointerImagePosition, + } + + provide(CanvasProviderKey, state) + return state +} + +export type CanvasProviderState = ReturnType + +export function useCanvasInject() { + return inject(CanvasProviderKey)! +} diff --git a/packages/core/src/components/editor/canvas/context/editor-mode.ts b/packages/core/src/components/editor/canvas/context/editor-mode.ts new file mode 100644 index 0000000..e820453 --- /dev/null +++ b/packages/core/src/components/editor/canvas/context/editor-mode.ts @@ -0,0 +1,26 @@ +import type { InjectionKey, Ref } from 'vue' +import { inject, provide } from 'vue' + +// ── 编辑器模式 ── +export type EditorMode = 'editor' | 'runtime' + +export const EditorModeKey: InjectionKey = Symbol('editor-mode') + +export function provideEditorMode(mode: EditorMode) { + provide(EditorModeKey, mode) +} + +export function useEditorModeInject(): EditorMode { + return inject(EditorModeKey, 'editor') +} + +// ── 预览缩放 ── +export const PreviewScaleKey: InjectionKey> = Symbol('preview-scale') + +export function providePreviewScale(scale: Ref) { + provide(PreviewScaleKey, scale) +} + +export function usePreviewScaleInject(): Ref | undefined { + return inject(PreviewScaleKey, undefined) +} diff --git a/packages/core/src/components/editor/canvas/context/index.ts b/packages/core/src/components/editor/canvas/context/index.ts new file mode 100644 index 0000000..a5cc915 --- /dev/null +++ b/packages/core/src/components/editor/canvas/context/index.ts @@ -0,0 +1,6 @@ +export * from './canvas' +export * from './editor-mode' +export * from './page' +export * from './runtime' +export * from './state' +export * from './ui' diff --git a/packages/core/src/components/editor/canvas/context/page.ts b/packages/core/src/components/editor/canvas/context/page.ts new file mode 100644 index 0000000..06808fb --- /dev/null +++ b/packages/core/src/components/editor/canvas/context/page.ts @@ -0,0 +1,54 @@ +import { storeToRefs } from 'pinia' +import { inject, provide, ref } from 'vue' +import pinia, { useCanvasStore } from '@/stores' + +const CanvasPageKey = Symbol('page') +export function useCanvasPageProvider() { + const canvasStore = useCanvasStore(pinia) + const { canvasList } = storeToRefs(canvasStore) + + function findPageById(pageId: string) { + return canvasList.value.find(page => page.id === pageId) || null + } + + // 画布管理弹窗(新增/重命名) + const showPageDialog = ref(false) + function changePageDialogVisible(visible: boolean) { + showPageDialog.value = visible + } + + const editingPageId = ref(null) + + // 打开“新增画布”弹窗 + function openCreatePageDialog() { + editingPageId.value = null + showPageDialog.value = true + } + + // 打开“重命名画布”弹窗 + function openRenamePageDialog(pageId: string) { + const page = findPageById(pageId) + if (!page) { + return + } + + editingPageId.value = pageId + showPageDialog.value = true + } + + const state = { + showPageDialog, + editingPageId, + changePageDialogVisible, + openRenamePageDialog, + openCreatePageDialog, + } + + provide(CanvasPageKey, state) + + return state +} + +export function useCanvasPageInject() { + return inject>(CanvasPageKey)! +} diff --git a/packages/core/src/components/editor/canvas/context/runtime.ts b/packages/core/src/components/editor/canvas/context/runtime.ts new file mode 100644 index 0000000..59ff696 --- /dev/null +++ b/packages/core/src/components/editor/canvas/context/runtime.ts @@ -0,0 +1,372 @@ +import type { VariableValueType } from '@cslab-dcs/schema' +import type { ComputedRef, InjectionKey, Ref } from 'vue' +import type { DynamicProjectProperty } from '@/api' +import { storeToRefs } from 'pinia' +import { computed, inject, provide, ref, shallowRef, watch } from 'vue' +import { getDynamicProjectModulePropsApi, getDynamicProjectModulesApi } from '@/api' +import pinia, { useProjectStore } from '@/stores' +import { useCanvasState } from './state' + +export interface CanvasVariableOption { + label: string + value: string + children?: CanvasVariableOption[] +} + +export interface CanvasRuntimeVariable { + path: string + moduleLabel: string + moduleName: string + moduleDescribe?: string + propLabel: string + propName: string + describe?: string + unit?: string + type: VariableValueType + defaultValue?: unknown + value?: unknown +} + +export interface CanvasRuntimeState { + isLoadingVariables: Ref + variableLoadError: Ref + variableOptions: ComputedRef + variableMap: ComputedRef> + backgroundSampleColor: ComputedRef + autoTextColor: ComputedRef + ensureVariableCatalogLoaded: (force?: boolean) => Promise + refreshVariableCatalog: () => Promise + setBackgroundAverageColor: (value: string | null) => void +} + +export const CanvasRuntimeKey: InjectionKey = Symbol('canvas-runtime') +const HEX_COLOR_RE = /^#(?:[\dA-F]{3}|[\dA-F]{6})$/i + +// 统一只接受十六进制颜色,便于后续自动推导文字颜色。 +function isHexColor(value: string) { + return HEX_COLOR_RE.test(value.trim()) +} + +function normalizeColor(value: string | null | undefined) { + if (!value) { + return null + } + const normalized = value.trim() + return isHexColor(normalized) ? normalized : null +} + +export function getReadableTextColor(color: string) { + const normalized = normalizeColor(color) || '#FFFFFF' + const hex = normalized.slice(1) + const expanded = hex.length === 3 + ? hex.split('').map(char => `${char}${char}`).join('') + : hex + const red = Number.parseInt(expanded.slice(0, 2), 16) + const green = Number.parseInt(expanded.slice(2, 4), 16) + const blue = Number.parseInt(expanded.slice(4, 6), 16) + const brightness = (red * 299 + green * 587 + blue * 114) / 1000 + return brightness >= 160 ? '#111827' : '#FFFFFF' +} + +function inferVariableType(property: DynamicProjectProperty): VariableValueType { + // classify: 0-整数 1-浮点 2-字符串 3-列表 4-枚举 5-布尔 6-组分 7-已知变量 + switch (property.classify) { + case '5': + return 'digital' + case '2': + return 'string' + case '4': + return 'enum' + default: + return 'analog' + } +} + +function hasLayerBindingValue(binding: string | { value?: string } | Array | undefined): boolean { + if (!binding) { + return false + } + if (Array.isArray(binding)) { + return binding.some(item => hasLayerBindingValue(item)) + } + if (typeof binding === 'string') { + return Boolean(binding.trim()) + } + return typeof binding.value === 'string' && Boolean(binding.value.trim()) +} + +export function useCanvasRuntimeProvider() { + const projectStore = useProjectStore(pinia) + const { projectId, projectInfo } = storeToRefs(projectStore) + const { canvasInfo, canvasView, layerList } = useCanvasState() + + // 变量目录来自基础项目接口,和当前画布上已绑定的变量值分开维护。 + const runtimeVariables = shallowRef([]) + const isLoadingVariables = ref(false) + const variableLoadError = ref('') + const backgroundAverageColor = shallowRef(null) + const loadedBaseProjectId = ref('') + // 通过递增 token 丢弃过期请求,避免快速切画布时旧响应覆盖新状态。 + let requestToken = 0 + + function setBackgroundAverageColor(value: string | null) { + backgroundAverageColor.value = normalizeColor(value) + } + + const canvasVarMap = computed(() => { + const map = new Map() + for (const variable of canvasInfo.value?.vars || []) { + const name = variable.name?.trim() + if (!name) { + continue + } + if (variable.defaultValue !== undefined) { + map.set(name, variable.defaultValue) + } + if (variable.sourceId?.trim()) { + map.set(variable.sourceId.trim(), variable.defaultValue) + } + } + return map + }) + + const variableMap = computed>(() => { + // 运行时变量以接口目录为主,画布上已有的默认值优先覆盖展示值。 + return runtimeVariables.value.reduce>((result, item) => { + const canvasValue = canvasVarMap.value.get(item.path) ?? canvasVarMap.value.get(item.propLabel) + result[item.path] = { + ...item, + value: canvasValue !== undefined ? canvasValue : item.defaultValue, + } + return result + }, {}) + }) + + const variableOptions = computed(() => { + // 变量选择器按照模块分组,便于属性面板直接构造级联选项。 + const groups = new Map() + for (const item of runtimeVariables.value) { + const group = groups.get(item.moduleName) || { + label: `${item.moduleName}(${item.moduleLabel})`, + value: item.moduleName, + children: [], + } + group.children!.push({ + label: item.propName, + value: item.path, + }) + groups.set(item.moduleName, group) + } + + return Array.from(groups.values(), (group) => { + group.children = group.children?.sort((left, right) => left.label.localeCompare(right.label)) + return group + }) + }) + + const backgroundSampleColor = computed(() => { + return backgroundAverageColor.value || normalizeColor(canvasView.value.background) || '#FFFFFF' + }) + + const autoTextColor = computed(() => getReadableTextColor(backgroundSampleColor.value)) + + const hasBoundLayers = computed(() => { + // 只有确实存在绑定的图层时才去拉变量目录,避免无意义请求。 + return layerList.value.some(layer => Object.values(layer.bindings || {}).some(binding => hasLayerBindingValue(binding))) + }) + + async function resolveBaseProjectId() { + if (!projectId.value) { + console.warn('[canvas-runtime] resolveBaseProjectId: projectId 为空,无法加载变量目录') + return '' + } + + const cachedBaseProjectId = projectInfo.value?.base_pro?.trim() + if (cachedBaseProjectId) { + return cachedBaseProjectId + } + + await projectStore.getProjectInfo() + const resolved = projectInfo.value?.base_pro?.trim() || '' + if (!resolved) { + console.warn('[canvas-runtime] resolveBaseProjectId: 项目 base_pro 为空', { + projectId: projectId.value, + projectInfo: projectInfo.value, + }) + } + return resolved + } + + function resetVariableCatalog() { + requestToken += 1 + runtimeVariables.value = [] + isLoadingVariables.value = false + loadedBaseProjectId.value = '' + } + + async function refreshVariableCatalog() { + const baseProjectId = await resolveBaseProjectId() + if (!baseProjectId) { + resetVariableCatalog() + return + } + + if (isLoadingVariables.value && loadedBaseProjectId.value === baseProjectId) { + return + } + + const currentToken = ++requestToken + isLoadingVariables.value = true + variableLoadError.value = '' + try { + // 先取模块,再并行拉每个模块下的属性定义,最后拍平成路径字典。 + console.log('[canvas-runtime] 正在加载变量目录', { baseProjectId }) + const modules = await getDynamicProjectModulesApi(baseProjectId) + console.log('[canvas-runtime] 模块列表', { count: modules.length, modules }) + if (!modules.length) { + console.warn('[canvas-runtime] module list is empty', { baseProjectId }) + } + + // 同 label 的模块属性定义完全一致,按 label 去重请求,避免重复调用。 + const labelToPk = new Map() + for (const m of modules) { + if (!labelToPk.has(m.label)) { + labelToPk.set(m.label, m.module_pk) + } + } + const propsByLabel = new Map() + await Promise.all( + Array.from(labelToPk.entries(), async ([label, modulePk]) => { + try { + const props = await getDynamicProjectModulePropsApi(baseProjectId, modulePk) + if (!props.length) { + console.warn('[canvas-runtime] module props are empty', { + baseProjectId, + moduleLabel: label, + modulePk, + }) + } + propsByLabel.set(label, props) + } + catch (error) { + console.error('[canvas-runtime] failed to load module props', label, error) + propsByLabel.set(label, []) + } + }), + ) + const propGroups = modules.map(module => ({ + module, + props: propsByLabel.get(module.label) || [], + })) + + if (currentToken !== requestToken) { + return + } + + // 按 path 去重:模块列表或属性列表可能包含重复项,只保留首次出现的 + const seenPaths = new Set() + runtimeVariables.value = propGroups.flatMap(({ module, props }) => { + return props.reduce((acc, prop) => { + const path = `${module.name}.${prop.name}` + if (!seenPaths.has(path)) { + seenPaths.add(path) + acc.push({ + path, + moduleLabel: module.label, + moduleName: module.name, + moduleDescribe: module.describe, + propLabel: prop.name, + propName: prop.displayLabel, + describe: prop.describe, + type: inferVariableType(prop), + defaultValue: prop.defaultValue, + }) + } + return acc + }, []) + }) + + if (!runtimeVariables.value.length) { + variableLoadError.value = modules.length + ? `已加载 ${modules.length} 个模块,但未获取到属性数据` + : '模块列表为空,请检查基础项目配置' + console.warn('[canvas-runtime] runtime variable catalog is empty after normalization', { + baseProjectId, + moduleCount: modules.length, + propGroupCount: propGroups.length, + }) + } + loadedBaseProjectId.value = baseProjectId + } + catch (error) { + if (currentToken === requestToken) { + runtimeVariables.value = [] + loadedBaseProjectId.value = '' + variableLoadError.value = `加载变量失败: ${error instanceof Error ? error.message : '未知错误'}` + } + console.error('[canvas-runtime] failed to load variable catalog', error) + } + finally { + if (currentToken === requestToken) { + isLoadingVariables.value = false + } + } + } + + async function ensureVariableCatalogLoaded(force = false) { + const baseProjectId = await resolveBaseProjectId() + if (!baseProjectId) { + resetVariableCatalog() + variableLoadError.value = '项目未关联动态项目,无法加载变量目录' + return + } + + if (!force && loadedBaseProjectId.value === baseProjectId) { + return + } + + await refreshVariableCatalog() + } + + watch( + () => projectId.value, + (nextProjectId, previousProjectId) => { + // 项目切换后变量目录必须整体失效,防止串用上一项目的变量。 + if (!nextProjectId || nextProjectId !== previousProjectId) { + resetVariableCatalog() + } + }, + { immediate: true }, + ) + + watch( + () => [projectId.value, hasBoundLayers.value], + ([nextProjectId, nextHasBoundLayers]) => { + // 只有项目已就绪且画布真的用到了绑定变量时,才懒加载变量目录。 + if (!nextProjectId || !nextHasBoundLayers) { + return + } + void ensureVariableCatalogLoaded() + }, + { immediate: true }, + ) + + const state: CanvasRuntimeState = { + isLoadingVariables, + variableLoadError, + variableOptions, + variableMap, + backgroundSampleColor, + autoTextColor, + ensureVariableCatalogLoaded, + refreshVariableCatalog, + setBackgroundAverageColor, + } + + provide(CanvasRuntimeKey, state) + return state +} + +export function useCanvasRuntimeInject() { + return inject(CanvasRuntimeKey)! +} diff --git a/packages/core/src/components/editor/canvas/context/state.ts b/packages/core/src/components/editor/canvas/context/state.ts new file mode 100644 index 0000000..404fe1d --- /dev/null +++ b/packages/core/src/components/editor/canvas/context/state.ts @@ -0,0 +1,46 @@ +import { storeToRefs } from 'pinia' +import pinia, { useCanvasStore, useLayerStore } from '@/stores' + +export function useCanvasState() { + const canvasStore = useCanvasStore(pinia) + const { + canvasId, + canvasList, + canvasZoom, + isHydratingCanvas, + canvasView, + activeCanvasThumbnail, + canvasInfo, + } = storeToRefs(canvasStore) + + const layerStore = useLayerStore(pinia) + const { + selectedLayerId, + selectedLayerIds, + selectedGroupId, + isBaseLayerActive, + selectedLayer, + selectedGroup, + layerList, + boxSelect, + } = storeToRefs(layerStore) + + // 将两个 store 的核心响应式字段收敛到一个组合入口,便于编辑器各模块共享。 + return { + canvasId, + canvasList, + canvasZoom, + isHydratingCanvas, + canvasView, + activeCanvasThumbnail, + canvasInfo, + selectedLayerId, + selectedLayerIds, + selectedGroupId, + isBaseLayerActive, + selectedLayer, + selectedGroup, + layerList, + boxSelect, + } +} diff --git a/packages/core/src/components/editor/canvas/context/ui.clipboard.ts b/packages/core/src/components/editor/canvas/context/ui.clipboard.ts new file mode 100644 index 0000000..962b08a --- /dev/null +++ b/packages/core/src/components/editor/canvas/context/ui.clipboard.ts @@ -0,0 +1,74 @@ +import type { Layer } from '../types' +import { computed, ref } from 'vue' +import pinia, { useLayerStore } from '@/stores' +import { useLayerSelection } from '../composables/layer' +import { useCanvasState } from '../context/state' +import { getLayerGroupById, getLayerGroupMeta, remapLayerGroupIds } from '../grouping' +import { clamp, cloneValue, createLayerId, snapToGrid } from '../utils' + +const GRID_SIZE = 10 +const PASTE_OFFSET = 20 + +export function useClipboard() { + const { canvasView, layerList, selectedLayerIds } = useCanvasState() + const layerStore = useLayerStore(pinia) + + const copiedLayers = ref([]) + const pasteRound = ref(0) + const hasCopiedLayers = computed(() => copiedLayers.value.length > 0) + + function copySelected() { + if (!selectedLayerIds.value.length) { + copiedLayers.value = [] + return + } + const selectedSet = new Set(selectedLayerIds.value) + copiedLayers.value = layerList.value + .filter(layer => selectedSet.has(layer.id)) + .map(layer => cloneValue(layer)) + pasteRound.value = 0 + } + + const { setSelection } = useLayerSelection() + function pasteCopied() { + if (!copiedLayers.value.length || !canvasView.value.imageWidth || !canvasView.value.imageHeight) { + return + } + + pasteRound.value += 1 + const offset = PASTE_OFFSET * pasteRound.value + + const nextLayers = remapLayerGroupIds(copiedLayers.value).map((layer) => { + const nextX = clamp(snapToGrid(layer.x + offset, GRID_SIZE), 0, Math.max(0, canvasView.value.imageWidth - layer.width)) + const nextY = clamp(snapToGrid(layer.y + offset, GRID_SIZE), 0, Math.max(0, canvasView.value.imageHeight - layer.height)) + return { + ...cloneValue(layer), + id: createLayerId(), + x: nextX, + y: nextY, + } + }) + + layerStore.appendLayers(nextLayers) + const nextIds = nextLayers.map(layer => layer.id) + const pastedGroupId = nextLayers[0] ? getLayerGroupMeta(nextLayers[0])?.groupId ?? '' : '' + const pastedGroup = getLayerGroupById(nextLayers, pastedGroupId) + if (pastedGroup && pastedGroup.layerIds.length === nextIds.length && nextIds.length > 1) { + setSelection(nextIds, nextIds[0] ?? null, { groupId: pastedGroup.groupId }) + return + } + setSelection(nextIds, nextIds[0] ?? null) + } + + function resetClipboard() { + copiedLayers.value = [] + pasteRound.value = 0 + } + + return { + copySelected, + pasteCopied, + resetClipboard, + hasCopiedLayers, + } +} diff --git a/packages/core/src/components/editor/canvas/context/ui.ts b/packages/core/src/components/editor/canvas/context/ui.ts new file mode 100644 index 0000000..5141d43 --- /dev/null +++ b/packages/core/src/components/editor/canvas/context/ui.ts @@ -0,0 +1,36 @@ +import { computed, inject, provide, ref } from 'vue' +import { useClipboard } from './ui.clipboard' + +const EditorUIKey = Symbol('ui') +export function useEditorUIProvider() { + // 画布工具状态 + const activeCanvasTool = ref('cursor') + // 光标样式直接由工具态派生,避免模板层再写一套判断。 + const canvasCursorMode = computed(() => (activeCanvasTool.value === 'move' ? 'move' : 'default')) + // 设置当前激活的画布工具 + function setActiveCanvasTool(tool: string) { + activeCanvasTool.value = tool + } + + // 是否按下空格键(用于临时切换到抓手工具) + const isSpacePressed = ref(false) + + const clipboard = useClipboard() + const state = { + activeCanvasTool, + canvasCursorMode, + setActiveCanvasTool, + isSpacePressed, + clipboard, + } + + provide(EditorUIKey, state) + + return state +} + +export type CanvasUIProvicerState = ReturnType + +export function useEditorUIInject() { + return inject(EditorUIKey)! +} diff --git a/packages/core/src/components/editor/canvas/emitter.ts b/packages/core/src/components/editor/canvas/emitter.ts new file mode 100644 index 0000000..9dd98a8 --- /dev/null +++ b/packages/core/src/components/editor/canvas/emitter.ts @@ -0,0 +1,92 @@ +import type { ComponentType } from '@cslab-dcs/schema' +import type { ResizeHandle } from './types' +import mitt from 'mitt' + +interface PositionUpdate { + id: string + nextX: number + nextY: number +} + +interface RectUpdate extends PositionUpdate { + nextWidth: number + nextHeight: number + options?: { ignoreLock?: boolean } +} + +interface ViewportTargetBounds { + x: number + y: number + width: number + height: number +} + +export interface EditorEventMap { + 'componentPalette:dragStart': { event: DragEvent, type: ComponentType } + 'componentPalette:pointerDown': { event: PointerEvent, type: ComponentType } + 'canvasStage:dragOver': DragEvent + 'canvasStage:drop': { event: DragEvent, type: string } + 'canvasStage:backgroundPointerDown': PointerEvent + 'canvasStage:layerClick': { event: MouseEvent, id: string } + 'canvasStage:layerPointerDown': { event: PointerEvent, id: string } + 'canvasStage:layerResizePointerDown': { event: PointerEvent, id: string, handle: ResizeHandle } + 'canvasStage:groupPointerDown': { event: PointerEvent, groupId: string } + 'canvasStage:groupResizePointerDown': { event: PointerEvent, groupId: string, handle: ResizeHandle } + 'canvasStage:baseLayerResizePointerDown': { event: PointerEvent, handle: ResizeHandle } + 'selection:backgroundPointerDown': PointerEvent + 'baseLayer:setSize': { width: number, height: number, options?: { resetView?: boolean } } + 'viewport:beginPan': PointerEvent + 'viewport:setScale': { newScale: number, anchor?: { clientX: number, clientY: number } } + 'viewport:panBy': { deltaX: number, deltaY: number } + 'viewport:centerTarget': ViewportTargetBounds + 'layer:updatePosition': { id: string, nextX: number, nextY: number } + 'layer:updatePositions': PositionUpdate[] + 'layer:updateRect': RectUpdate + 'layer:updateRects': RectUpdate[] + 'layer:updateField': { id: string, field: string, value: unknown, options?: { ignoreLock?: boolean } } + 'layer:updateFields': { ids: string[], field: string, value: unknown, options?: { ignoreLock?: boolean } } + 'propertyPanel:panelPointerdown': PointerEvent + 'propertyPanel:suspend': undefined + 'propertyPanel:resume': undefined + 'propertyPanel:openPanel': undefined + 'propertyPanel:closePanel': undefined + 'propertyPanel:replaceBackground': { source: string } + 'propertyPanel:removeBackground': undefined + 'header:undo': undefined + 'header:redo': undefined + 'history:stateChange': { canUndo: boolean, canRedo: boolean, undoLabel: string, redoLabel: string } + 'history:push': { label: string } +} + +type EventName = keyof EditorEventMap +type VoidEventName = { + [K in EventName]: EditorEventMap[K] extends undefined ? K : never +}[EventName] +type ValueEventName = Exclude + +const emitter = mitt>() + +export function setEditorEmitter(type: K): void +export function setEditorEmitter(type: K, value: EditorEventMap[K]): void +export function setEditorEmitter(type: K, value?: EditorEventMap[K]) { + // 编辑器内部模块通过统一事件总线通信,尽量避免跨层级直接互相引用。 + emitter.emit(type, value as unknown) +} + +export function listenEditorEmitter( + type: K, + handler: (value: EditorEventMap[K]) => void, +) { + emitter.on(type, handler as (value: unknown) => void) +} + +export function removeEditorEmitter(types: EventName[] = []) { + if (!types.length) { + emitter.all.clear() + return + } + + types.forEach((type) => { + emitter.all.delete(type) + }) +} diff --git a/packages/core/src/components/editor/canvas/grouping.ts b/packages/core/src/components/editor/canvas/grouping.ts new file mode 100644 index 0000000..cee7b74 --- /dev/null +++ b/packages/core/src/components/editor/canvas/grouping.ts @@ -0,0 +1,221 @@ +import type { Layer, LayerGroup } from './types' +import { get } from 'es-toolkit/compat' + +type UnknownRecord = Record + +const MIXED_VALUE = Symbol('mixed-value') + +export function asRecord(value: unknown): UnknownRecord | null { + if (!value || typeof value !== 'object') { + return null + } + return value as UnknownRecord +} + +function readStringValue(source: UnknownRecord | null, keys: string[]) { + if (!source) { + return null + } + + for (const key of keys) { + const value = source[key] + if (typeof value !== 'string') { + continue + } + const normalized = value.trim() + if (normalized) { + return normalized + } + } + + return null +} + +export function createLayerGroupId() { + return `group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` +} + +export function getLayerGroupMeta(layer: Layer): { groupId: string, groupName: string } | null { + const layerRecord = asRecord(layer) + const configRecord = asRecord(layer.config) + const layerMetadataRecord = asRecord(layerRecord?.metadata) + const configMetadataRecord = asRecord(configRecord?.metadata) + const sources = [layerRecord, layerMetadataRecord, configRecord, configMetadataRecord] + + for (const source of sources) { + const groupId = readStringValue(source, ['groupId', 'group_id']) + if (!groupId) { + continue + } + + const groupName = readStringValue(source, ['groupName', 'group_name', 'groupLabel']) + || `分组-${groupId.slice(-6)}` + return { + groupId, + groupName, + } + } + + return null +} + +export function withLayerGroupMeta( + layer: Layer, + meta: { groupId: string, groupName: string } | null, +) { + const currentMetadata = asRecord(layer.config?.metadata) ?? {} + const nextMetadata = { + ...currentMetadata, + } + + delete nextMetadata.groupId + delete nextMetadata.groupName + delete nextMetadata.group_id + delete nextMetadata.group_name + + if (meta) { + nextMetadata.groupId = meta.groupId + nextMetadata.groupName = meta.groupName + } + + return { + ...layer, + config: { + ...layer.config, + metadata: nextMetadata, + }, + } +} + +export function buildLayerGroups(layers: Layer[]): LayerGroup[] { + const groups = new Map() + + layers.forEach((layer) => { + const meta = getLayerGroupMeta(layer) + if (!meta) { + return + } + + const existing = groups.get(meta.groupId) + if (existing) { + // 分组包围盒在遍历过程中持续扩张,避免额外二次扫描。 + const minX = Math.min(existing.x, layer.x) + const minY = Math.min(existing.y, layer.y) + const maxX = Math.max(existing.x + existing.width, layer.x + layer.width) + const maxY = Math.max(existing.y + existing.height, layer.y + layer.height) + existing.layerIds.push(layer.id) + existing.layers.push(layer) + existing.x = minX + existing.y = minY + existing.width = maxX - minX + existing.height = maxY - minY + return + } + + groups.set(meta.groupId, { + groupId: meta.groupId, + groupName: meta.groupName, + layerIds: [layer.id], + layers: [layer], + x: layer.x, + y: layer.y, + width: layer.width, + height: layer.height, + }) + }) + + return [...groups.values()] +} + +export function getLayerGroupById(layers: Layer[], groupId: string) { + if (!groupId) { + return null + } + return buildLayerGroups(layers).find(group => group.groupId === groupId) ?? null +} + +export function getLayerGroupByLayerId(layers: Layer[], layerId: string) { + if (!layerId) { + return null + } + return buildLayerGroups(layers).find(group => group.layerIds.includes(layerId)) ?? null +} + +export function remapLayerGroupIds(layers: Layer[]) { + const groupIdMap = new Map() + + return layers.map((layer) => { + const meta = getLayerGroupMeta(layer) + if (!meta) { + return layer + } + + const nextGroupId = groupIdMap.get(meta.groupId) ?? createLayerGroupId() + if (!groupIdMap.has(meta.groupId)) { + groupIdMap.set(meta.groupId, nextGroupId) + } + + return withLayerGroupMeta(layer, { + groupId: nextGroupId, + groupName: meta.groupName, + }) + }) +} + +export function buildGroupPositionUpdates( + layers: Layer[], + deltaX: number, + deltaY: number, +) { + return layers.map(layer => ({ + id: layer.id, + nextX: Math.round(layer.x + deltaX), + nextY: Math.round(layer.y + deltaY), + })) +} + +export function buildGroupRectUpdates( + layers: Layer[], + currentBounds: Pick, + nextBounds: Pick, +) { + // 分组缩放按相对位置和比例计算成员新矩形,保持组内布局关系不变。 + const widthRatio = currentBounds.width === 0 ? 1 : nextBounds.width / currentBounds.width + const heightRatio = currentBounds.height === 0 ? 1 : nextBounds.height / currentBounds.height + + return layers.map((layer) => { + const relativeX = currentBounds.width === 0 ? 0 : (layer.x - currentBounds.x) / currentBounds.width + const relativeY = currentBounds.height === 0 ? 0 : (layer.y - currentBounds.y) / currentBounds.height + return { + id: layer.id, + nextX: Math.round(nextBounds.x + relativeX * nextBounds.width), + nextY: Math.round(nextBounds.y + relativeY * nextBounds.height), + nextWidth: Math.round(layer.width * widthRatio), + nextHeight: Math.round(layer.height * heightRatio), + } + }) +} + +function isSameValue(left: unknown, right: unknown) { + return JSON.stringify(left) === JSON.stringify(right) +} + +export function getCommonLayerFieldValue(layers: Layer[], field: string) { + if (!layers.length) { + return undefined + } + + const [firstLayer, ...restLayers] = layers + const firstValue = get(firstLayer, field) + + // 属性面板批量编辑时,用特殊标记区分“真的空值”和“多值混合”。 + if (restLayers.every(layer => isSameValue(get(layer, field), firstValue))) { + return firstValue + } + + return MIXED_VALUE +} + +export function isMixedGroupFieldValue(value: unknown): value is typeof MIXED_VALUE { + return value === MIXED_VALUE +} diff --git a/packages/core/src/components/editor/canvas/index.vue b/packages/core/src/components/editor/canvas/index.vue new file mode 100644 index 0000000..dfdbaa8 --- /dev/null +++ b/packages/core/src/components/editor/canvas/index.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/packages/core/src/components/editor/canvas/listeners/index.ts b/packages/core/src/components/editor/canvas/listeners/index.ts new file mode 100644 index 0000000..646a56b --- /dev/null +++ b/packages/core/src/components/editor/canvas/listeners/index.ts @@ -0,0 +1,3 @@ +export * from './key-pointer' +export * from './page' +export * from './wheel' diff --git a/packages/core/src/components/editor/canvas/listeners/key-pointer.ts b/packages/core/src/components/editor/canvas/listeners/key-pointer.ts new file mode 100644 index 0000000..823ba49 --- /dev/null +++ b/packages/core/src/components/editor/canvas/listeners/key-pointer.ts @@ -0,0 +1,435 @@ +import type { ComponentType } from '@cslab-dcs/schema' +import type { CanvasProviderState, CanvasUIProvicerState } from '../context' +import type { ResizeHandle } from '../types' +import { useEventListener } from '@vueuse/core' +import { reactive, ref } from 'vue' +import { MAX_CANVAS_SIZE, MIN_CANVAS_HEIGHT, MIN_CANVAS_WIDTH } from '@/constants' +import pinia, { useLayerStore } from '@/stores' +import { useLayerSelection } from '../composables/layer' +import { useCanvasState } from '../context/state' +import { listenEditorEmitter, setEditorEmitter } from '../emitter' + +// 工具栏创建工具 ID → 组件类型的映射 +const CREATION_TOOL_TYPE_MAP: Record = { + frame: 'text', + rect: 'rect', + number: 'number', + button: 'button', +} + +function isEditableElement(target: EventTarget | null) { + if (!(target instanceof HTMLElement)) { + return false + } + return target.isContentEditable + || target.tagName === 'INPUT' + || target.tagName === 'TEXTAREA' + || target.tagName === 'SELECT' +} + +function hasNativeTextSelection() { + if (typeof window === 'undefined') { + return false + } + + const selection = window.getSelection() + return Boolean(selection && !selection.isCollapsed && selection.toString().trim()) +} + +function resolveHtmlElement(target: EventTarget | null) { + return target instanceof HTMLElement ? target : null +} + +function isLayerCopyShortcutContext(provider: CanvasProviderState, event: KeyboardEvent) { + const stageWrapper = provider.getWrapperElement() + const candidates = [ + resolveHtmlElement(event.target), + resolveHtmlElement(typeof document !== 'undefined' ? document.activeElement : null), + ].filter(Boolean) as HTMLElement[] + + return candidates.some((candidate) => { + if (stageWrapper?.contains(candidate)) { + return true + } + + return Boolean(candidate.closest('[data-layer-list-shortcuts="true"]')) + }) +} + +// 底图锁定宽高比时,拖动单边控制点也要回推另一边的尺寸。 +function normalizeLockedSizeByWidth(width: number, ratio: number) { + let nextWidth = Math.min(MAX_CANVAS_SIZE, Math.max(MIN_CANVAS_WIDTH, Math.round(width))) + let nextHeight = Math.round(nextWidth / ratio) + + if (nextHeight < MIN_CANVAS_HEIGHT) { + nextHeight = MIN_CANVAS_HEIGHT + nextWidth = Math.round(nextHeight * ratio) + } + if (nextWidth < MIN_CANVAS_WIDTH) { + nextWidth = MIN_CANVAS_WIDTH + nextHeight = Math.round(nextWidth / ratio) + } + if (nextHeight > MAX_CANVAS_SIZE) { + nextHeight = MAX_CANVAS_SIZE + nextWidth = Math.round(nextHeight * ratio) + } + if (nextWidth > MAX_CANVAS_SIZE) { + nextWidth = MAX_CANVAS_SIZE + nextHeight = Math.round(nextWidth / ratio) + } + + return { + nextWidth: Math.min(MAX_CANVAS_SIZE, Math.max(MIN_CANVAS_WIDTH, nextWidth)), + nextHeight: Math.min(MAX_CANVAS_SIZE, Math.max(MIN_CANVAS_HEIGHT, nextHeight)), + } +} + +function normalizeLockedSizeByHeight(height: number, ratio: number) { + let nextHeight = Math.min(MAX_CANVAS_SIZE, Math.max(MIN_CANVAS_HEIGHT, Math.round(height))) + let nextWidth = Math.round(nextHeight * ratio) + + if (nextWidth < MIN_CANVAS_WIDTH) { + nextWidth = MIN_CANVAS_WIDTH + nextHeight = Math.round(nextWidth / ratio) + } + if (nextHeight < MIN_CANVAS_HEIGHT) { + nextHeight = MIN_CANVAS_HEIGHT + nextWidth = Math.round(nextHeight * ratio) + } + if (nextWidth > MAX_CANVAS_SIZE) { + nextWidth = MAX_CANVAS_SIZE + nextHeight = Math.round(nextWidth / ratio) + } + if (nextHeight > MAX_CANVAS_SIZE) { + nextHeight = MAX_CANVAS_SIZE + nextWidth = Math.round(nextHeight * ratio) + } + + return { + nextWidth: Math.min(MAX_CANVAS_SIZE, Math.max(MIN_CANVAS_WIDTH, nextWidth)), + nextHeight: Math.min(MAX_CANVAS_SIZE, Math.max(MIN_CANVAS_HEIGHT, nextHeight)), + } +} + +export function useKeyAndPointerListener(provider: CanvasProviderState, uiProvider: CanvasUIProvicerState, history?: { undo: () => void, redo: () => void }) { + // 这里负责“全局输入”,包括快捷键、面板外拖放和底图缩放手柄。 + const isPointerDragging = ref(false) + const fallbackDragType = ref('number') + + const baseLayerResizeState = reactive({ + active: false, + handle: null as ResizeHandle | null, + startX: 0, + startY: 0, + startWidth: 0, + startHeight: 0, + }) + + const { canvasView } = useCanvasState() + const { + clearSelection, + removeSelectedLayers, + bringSelectedToFront, + sendSelectedToBack, + groupSelected, + ungroupSelected, + placeLayerAtClientPoint, + } = useLayerSelection() + + useEventListener(window, 'pointerup', (event) => { + if (!isPointerDragging.value) { + return + } + isPointerDragging.value = false + setEditorEmitter('propertyPanel:resume') + + const pointer = provider.getPointerImagePosition(event.clientX, event.clientY) + + // 非原生 drag 模式下,靠 pointerup 时的位置补一个”放入画布”动作。 + const placed = placeLayerAtClientPoint(pointer, fallbackDragType.value) + if (placed) { + setEditorEmitter('history:push', { label: '添加图层' }) + } + }) + + function getPointerImagePosition(clientX: number, clientY: number) { + const canvas = provider.getCanvasElement() + if (!canvas || canvasView.value.scale <= 0) { + return null + } + const rect = canvas.getBoundingClientRect() + const canvasX = clientX - rect.left + const canvasY = clientY - rect.top + + return { + imageX: (canvasX - canvasView.value.offsetX) / canvasView.value.scale, + imageY: (canvasY - canvasView.value.offsetY) / canvasView.value.scale, + } + } + + function onBaseLayerResizePointerMove(event: PointerEvent) { + if (!baseLayerResizeState.active || !baseLayerResizeState.handle) { + return + } + + const pointer = getPointerImagePosition(event.clientX, event.clientY) + if (!pointer) { + return + } + + const deltaX = pointer.imageX - baseLayerResizeState.startX + const deltaY = pointer.imageY - baseLayerResizeState.startY + let nextWidth = baseLayerResizeState.startWidth + let nextHeight = baseLayerResizeState.startHeight + + if (baseLayerResizeState.handle.includes('e')) { + nextWidth = baseLayerResizeState.startWidth + deltaX + } + if (baseLayerResizeState.handle.includes('w')) { + nextWidth = baseLayerResizeState.startWidth - deltaX + } + if (baseLayerResizeState.handle.includes('s')) { + nextHeight = baseLayerResizeState.startHeight + deltaY + } + if (baseLayerResizeState.handle.includes('n')) { + nextHeight = baseLayerResizeState.startHeight - deltaY + } + + if (canvasView.value.backgroundLockAspectRatio && baseLayerResizeState.startWidth > 0 && baseLayerResizeState.startHeight > 0) { + const ratio = baseLayerResizeState.startWidth / baseLayerResizeState.startHeight + const hasHorizontalHandle = baseLayerResizeState.handle.includes('e') || baseLayerResizeState.handle.includes('w') + const hasVerticalHandle = baseLayerResizeState.handle.includes('n') || baseLayerResizeState.handle.includes('s') + + if (hasHorizontalHandle && !hasVerticalHandle) { + const normalized = normalizeLockedSizeByWidth(nextWidth, ratio) + nextWidth = normalized.nextWidth + nextHeight = normalized.nextHeight + } + else if (!hasHorizontalHandle && hasVerticalHandle) { + const normalized = normalizeLockedSizeByHeight(nextHeight, ratio) + nextWidth = normalized.nextWidth + nextHeight = normalized.nextHeight + } + else { + const widthRatioDelta = Math.abs(nextWidth - baseLayerResizeState.startWidth) / Math.max(1, baseLayerResizeState.startWidth) + const heightRatioDelta = Math.abs(nextHeight - baseLayerResizeState.startHeight) / Math.max(1, baseLayerResizeState.startHeight) + if (widthRatioDelta >= heightRatioDelta) { + const normalized = normalizeLockedSizeByWidth(nextWidth, ratio) + nextWidth = normalized.nextWidth + nextHeight = normalized.nextHeight + } + else { + const normalized = normalizeLockedSizeByHeight(nextHeight, ratio) + nextWidth = normalized.nextWidth + nextHeight = normalized.nextHeight + } + } + } + else { + nextWidth = Math.min(MAX_CANVAS_SIZE, Math.max(MIN_CANVAS_WIDTH, Math.round(nextWidth))) + nextHeight = Math.min(MAX_CANVAS_SIZE, Math.max(MIN_CANVAS_HEIGHT, Math.round(nextHeight))) + } + + // 底图尺寸统一通过 emitter 分发,和属性面板改尺寸共用一套入口。 + setEditorEmitter('baseLayer:setSize', { width: nextWidth, height: nextHeight }) + } + + useEventListener(window, 'pointermove', (event: PointerEvent) => { + onBaseLayerResizePointerMove(event) + }) + + function onBaseLayerResizePointerEnd() { + if (!baseLayerResizeState.active) { + return + } + baseLayerResizeState.active = false + baseLayerResizeState.handle = null + setEditorEmitter('propertyPanel:resume') + } + + useEventListener(window, 'pointerup', () => { + onBaseLayerResizePointerEnd() + }) + + useEventListener(window, 'pointercancel', () => { + isPointerDragging.value = false + setEditorEmitter('propertyPanel:resume') + onBaseLayerResizePointerEnd() + }) + + useEventListener(window, 'keydown', (event: KeyboardEvent) => { + if (event.code === 'Space' && !isEditableElement(event.target)) { + uiProvider.isSpacePressed.value = true + event.preventDefault() + return + } + + if (isEditableElement(event.target)) { + return + } + + const key = event.key.toLowerCase() + const withMeta = event.metaKey || event.ctrlKey + + if (withMeta && !event.shiftKey && key === 'z') { + if (history) { + history.undo() + } + else { + setEditorEmitter('header:undo') + } + event.preventDefault() + return + } + if (withMeta && event.shiftKey && key === 'z') { + if (history) { + history.redo() + } + else { + setEditorEmitter('header:redo') + } + event.preventDefault() + return + } + + // 编辑器快捷键只在非输入态生效,避免和表单输入冲突。 + if (withMeta && key === 'c') { + if (hasNativeTextSelection()) { + return + } + if (!isLayerCopyShortcutContext(provider, event)) { + return + } + uiProvider.clipboard.copySelected() + event.preventDefault() + return + } + + if (withMeta && key === 'v') { + uiProvider.clipboard.pasteCopied() + event.preventDefault() + return + } + + if (event.key === 'Delete' || event.key === 'Backspace') { + event.preventDefault() + removeSelectedLayers().then(() => { + setEditorEmitter('history:push', { label: '删除图层' }) + }) + return + } + + if (event.altKey && event.key === ']') { + bringSelectedToFront() + setEditorEmitter('history:push', { label: '调整层级' }) + event.preventDefault() + return + } + + if (event.altKey && event.key === '[') { + sendSelectedToBack() + setEditorEmitter('history:push', { label: '调整层级' }) + event.preventDefault() + return + } + + if (withMeta && !event.shiftKey && key === 'g') { + const grouped = groupSelected() + if (grouped) { + setEditorEmitter('history:push', { label: '创建分组' }) + event.preventDefault() + } + return + } + + if (withMeta && event.shiftKey && key === 'g') { + event.preventDefault() + ungroupSelected().then((ungrouped) => { + if (ungrouped) { + setEditorEmitter('history:push', { label: '解除分组' }) + } + }) + } + }) + + useEventListener(window, 'keyup', (event: KeyboardEvent) => { + if (event.code === 'Space') { + uiProvider.isSpacePressed.value = false + } + }) + + function onComponentPalettePointerDown({ event, type }: { event: PointerEvent, type: ComponentType }) { + isPointerDragging.value = true + fallbackDragType.value = type + setEditorEmitter('propertyPanel:suspend') + event.preventDefault() + } + listenEditorEmitter('componentPalette:pointerDown', onComponentPalettePointerDown) + + const layerStore = useLayerStore(pinia) + function onBaseLayerResizePointerDown(payload: { event: PointerEvent, handle: ResizeHandle }) { + const { event, handle } = payload + if (event.button !== 0 || !canvasView.value.imageWidth || !canvasView.value.imageHeight) { + return + } + + const pointer = getPointerImagePosition(event.clientX, event.clientY) + if (!pointer) { + return + } + + event.preventDefault() + setEditorEmitter('propertyPanel:suspend') + baseLayerResizeState.active = true + baseLayerResizeState.handle = handle + baseLayerResizeState.startX = pointer.imageX + baseLayerResizeState.startY = pointer.imageY + baseLayerResizeState.startWidth = canvasView.value.imageWidth + baseLayerResizeState.startHeight = canvasView.value.imageHeight + layerStore.activateBaseLayerSelection() + } + listenEditorEmitter('canvasStage:baseLayerResizePointerDown', onBaseLayerResizePointerDown) + + function isPointInsideBaseLayer(point: { imageX: number, imageY: number }) { + if (!canvasView.value.imageWidth || !canvasView.value.imageHeight) { + return false + } + return point.imageX >= 0 + && point.imageY >= 0 + && point.imageX <= canvasView.value.imageWidth + && point.imageY <= canvasView.value.imageHeight + } + + function onBackgroundPointerDown(event: PointerEvent) { + const shouldPan = event.button === 1 + || (event.button === 0 && (uiProvider.isSpacePressed.value || uiProvider.activeCanvasTool.value === 'move')) + if (shouldPan) { + // 空格或抓手工具下,背景点击优先进入视口平移模式。 + setEditorEmitter('viewport:beginPan', event) + return + } + + // 创建工具模式:点击画布直接在落点处创建对应类型的图层,然后切回选择工具。 + const creationType = CREATION_TOOL_TYPE_MAP[uiProvider.activeCanvasTool.value] + if (creationType && event.button === 0) { + const pointer = provider.getPointerImagePosition(event.clientX, event.clientY) + const placed = placeLayerAtClientPoint(pointer, creationType) + if (placed) { + setEditorEmitter('history:push', { label: '添加图层' }) + } + uiProvider.setActiveCanvasTool('cursor') + return + } + + const pointer = provider.getPointerImagePosition(event.clientX, event.clientY) + if (!pointer || !isPointInsideBaseLayer(pointer)) { + layerStore.deactivateBaseLayerSelection() + clearSelection() + return + } + + layerStore.activateBaseLayerSelection() + setEditorEmitter('selection:backgroundPointerDown', event) + } + listenEditorEmitter('canvasStage:backgroundPointerDown', onBackgroundPointerDown) +} diff --git a/packages/core/src/components/editor/canvas/listeners/page.ts b/packages/core/src/components/editor/canvas/listeners/page.ts new file mode 100644 index 0000000..4ca4dba --- /dev/null +++ b/packages/core/src/components/editor/canvas/listeners/page.ts @@ -0,0 +1,10 @@ +import { useEventListener } from '@vueuse/core' +import { useLayerPersistence } from '../composables/persistence' + +export function usePageChangeListener() { + const { persistComponentsLayerNow } = useLayerPersistence() + useEventListener(window, 'pagehide', () => { + // 页面被隐藏或刷新前尽量再落盘一次,降低“刚拖完就刷新”导致的数据丢失概率。 + persistComponentsLayerNow() + }) +} diff --git a/packages/core/src/components/editor/canvas/listeners/wheel.ts b/packages/core/src/components/editor/canvas/listeners/wheel.ts new file mode 100644 index 0000000..afa4a15 --- /dev/null +++ b/packages/core/src/components/editor/canvas/listeners/wheel.ts @@ -0,0 +1,50 @@ +import type { ShallowRef } from 'vue' +import { useEventListener } from '@vueuse/core' +import { useCanvasState } from '../context/state' +import { setEditorEmitter } from '../emitter' + +// 鼠标位于侧边面板(图层/属性)时,不拦截滚轮,避免误触发画布平移。 +function isWheelOnPanel(target: EventTarget | null) { + if (!(target instanceof HTMLElement)) { + return false + } + return Boolean(target.closest('[data-editor-panel], .property-panel')) +} + +export function useWheelListener(editorRef: ShallowRef) { + const { canvasView } = useCanvasState() + + useEventListener( + editorRef, + 'wheel', + (event: WheelEvent) => { + if (isWheelOnPanel(event.target)) { + return + } + + if (event.ctrlKey || event.metaKey) { + event.preventDefault() + // 组合键滚轮走缩放,并以鼠标位置作为锚点。 + const factor = event.deltaY > 0 ? 1 / 1.08 : 1.08 + const newScale = canvasView.value.scale * factor + const anchor = { clientX: event.clientX, clientY: event.clientY } + setEditorEmitter('viewport:setScale', { newScale, anchor }) + + return + } + + const horizontalDelta = event.deltaX || (event.shiftKey ? event.deltaY : 0) + const verticalDelta = event.deltaY + if (!horizontalDelta && !verticalDelta) { + return + } + + event.preventDefault() + // 与常见滚动方向一致:滚轮向下时画面向上移动,向右时画面向左移动。 + const deltaX = -horizontalDelta + const deltaY = -verticalDelta + setEditorEmitter('viewport:panBy', { deltaX, deltaY }) + }, + { passive: false }, + ) +} diff --git a/packages/core/src/components/editor/canvas/modules/component-palette/constants.ts b/packages/core/src/components/editor/canvas/modules/component-palette/constants.ts new file mode 100644 index 0000000..7325a1c --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/component-palette/constants.ts @@ -0,0 +1,8 @@ +import type { ComponentPaletteItem } from './types' + +export const COMPONENT_PALETTE_ITEMS: ComponentPaletteItem[] = [ + { title: '棒图', type: 'bar', icon: 'fa-solid fa-mattress-pillow', description: '分段变色柱状指示' }, + { title: '控制仪表', type: 'pidController', icon: 'fa-solid fa-gauge-high', description: 'PID 控制仪表面板' }, + { title: '阀门控制器', type: 'valveController', icon: 'fa-solid fa-faucet', description: '阀门开度控制面板' }, + { title: '切页按钮', type: 'canvasSwitcher', icon: 'fa-solid fa-layer-group', description: '画布 Tab 切换' }, +] diff --git a/packages/core/src/components/editor/canvas/modules/component-palette/index.vue b/packages/core/src/components/editor/canvas/modules/component-palette/index.vue new file mode 100644 index 0000000..568d9de --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/component-palette/index.vue @@ -0,0 +1,153 @@ + + + + + diff --git a/packages/core/src/components/editor/canvas/modules/component-palette/types.ts b/packages/core/src/components/editor/canvas/modules/component-palette/types.ts new file mode 100644 index 0000000..8a13519 --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/component-palette/types.ts @@ -0,0 +1,8 @@ +import type { ComponentType } from '@cslab-dcs/schema' + +export interface ComponentPaletteItem { + title: string + type: ComponentType + icon: string + description?: string +} diff --git a/packages/core/src/components/editor/canvas/modules/editor-sidebar/components/tool-rail.vue b/packages/core/src/components/editor/canvas/modules/editor-sidebar/components/tool-rail.vue new file mode 100644 index 0000000..e180d34 --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/editor-sidebar/components/tool-rail.vue @@ -0,0 +1,193 @@ + + + + + diff --git a/packages/core/src/components/editor/canvas/modules/editor-sidebar/constants.ts b/packages/core/src/components/editor/canvas/modules/editor-sidebar/constants.ts new file mode 100644 index 0000000..ee02a86 --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/editor-sidebar/constants.ts @@ -0,0 +1,25 @@ +import type { EditorSidebarTabItem, EditorSidebarToolItem, EditorSidebarToolOption } from './types' + +export const EDITOR_SIDEBAR_MOVE_TOOL_OPTIONS: EditorSidebarToolOption[] = [ + { id: 'cursor', title: '选择工具', icon: 'fa-solid fa-arrow-pointer', shortcut: 'V' }, + { id: 'move', title: '移动视图', icon: 'fa-regular fa-hand', shortcut: 'H' }, +] + +export const EDITOR_SIDEBAR_TOOL_ITEMS: EditorSidebarToolItem[] = [ + { + id: 'move-tool', + title: '移动工具', + icon: 'fa-solid fa-location-arrow', + options: EDITOR_SIDEBAR_MOVE_TOOL_OPTIONS, + }, + { id: 'frame', title: '文本工具', icon: 'fa-solid fa-font', dividerBefore: true }, + { id: 'rect', title: '矩形工具', icon: 'fa-regular fa-square' }, + { id: 'number', title: '数值工具', icon: 'fa-solid fa-hashtag' }, + { id: 'button', title: '按钮工具', icon: 'fa-regular fa-hand-pointer' }, +] + +export const EDITOR_SIDEBAR_TABS: EditorSidebarTabItem[] = [ + { key: 'layers', label: '图层' }, + { key: 'components', label: '组件' }, + { key: 'templates', label: '模板' }, +] diff --git a/packages/core/src/components/editor/canvas/modules/editor-sidebar/index.vue b/packages/core/src/components/editor/canvas/modules/editor-sidebar/index.vue new file mode 100644 index 0000000..b503141 --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/editor-sidebar/index.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/packages/core/src/components/editor/canvas/modules/editor-sidebar/types.ts b/packages/core/src/components/editor/canvas/modules/editor-sidebar/types.ts new file mode 100644 index 0000000..d51a93c --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/editor-sidebar/types.ts @@ -0,0 +1,22 @@ +export type EditorSidebarTabKey = 'layers' | 'components' | 'templates' | 'resources' | 'ai' + +export interface EditorSidebarToolOption { + id: string + title: string + icon: string + shortcut?: string +} + +export interface EditorSidebarToolItem { + id: string + title: string + icon: string + dividerBefore?: boolean + options?: EditorSidebarToolOption[] +} + +export interface EditorSidebarTabItem { + key: EditorSidebarTabKey + label: string + showNew?: boolean +} diff --git a/packages/core/src/components/editor/canvas/modules/index.ts b/packages/core/src/components/editor/canvas/modules/index.ts new file mode 100644 index 0000000..170da62 --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/index.ts @@ -0,0 +1,3 @@ +export { default as EditorSidebar } from './editor-sidebar/index.vue' +export { default as PropertyPanel } from './property-panel/index.vue' +export { default as CanvasStage } from './stage/index.vue' diff --git a/packages/core/src/components/editor/canvas/modules/layers-panel/TagNumberView.vue b/packages/core/src/components/editor/canvas/modules/layers-panel/TagNumberView.vue new file mode 100644 index 0000000..197b2cc --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/layers-panel/TagNumberView.vue @@ -0,0 +1,157 @@ + + + + + diff --git a/packages/core/src/components/editor/canvas/modules/layers-panel/index.vue b/packages/core/src/components/editor/canvas/modules/layers-panel/index.vue new file mode 100644 index 0000000..72fc73a --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/layers-panel/index.vue @@ -0,0 +1,1611 @@ + + + + + diff --git a/packages/core/src/components/editor/canvas/modules/layers-panel/layer-row.scss b/packages/core/src/components/editor/canvas/modules/layers-panel/layer-row.scss new file mode 100644 index 0000000..580e56c --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/layers-panel/layer-row.scss @@ -0,0 +1,118 @@ +// 共享的图层行样式 — 供 index.vue 和 TagNumberView.vue 复用 + +.layer-search-row { + height: 36px; + display: flex; + align-items: center; + padding: 0 8px; + gap: 4px; + border-bottom: 1px solid var(--ed-border); +} + +.layer-list { + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + padding: 0 2px 8px 0; + display: flex; + flex-direction: column; + gap: 0; + contain: layout style; +} + +.layer-list:focus { + outline: none; +} + +.layer-row { + height: 30px; + border-radius: 0; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + position: relative; + padding-right: 0; +} + +.layer-row:not(.is-group-row):not(.active):not(.active-group):not(.is-renaming):hover { + background: #ffffff; + box-shadow: inset 0 0 0 1px var(--ed-accent); +} + +.layer-row.is-group-row { + font-weight: 500; +} + +.layer-row.is-group-row:hover { + background: var(--ed-bg-hover); +} + +.layer-left { + display: flex; + align-items: center; + min-width: 0; + position: relative; + flex: 1; + height: 100%; +} + +.layer-left.has-caret { + padding-left: 10px !important; +} + +.layer-icon-button, +.layer-name-button { + appearance: none; + border: none; + background: transparent; + padding: 0; + margin: 0; + color: inherit; + font: inherit; +} + +.layer-icon-button { + width: 16px; + height: 16px; + margin-left: 2px; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + flex: 0 0 auto; +} + +.layer-glyph { + font-size: 12px; + color: var(--ed-text-tertiary); +} + +.layer-name-button { + margin-left: 4px; + min-width: 0; + flex: 1; + text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 13px; + color: var(--ed-text-primary); + font-weight: 400; + cursor: pointer; +} + +.group-caret { + font-size: 12px; + color: var(--ed-text-tertiary); + transition: transform 0.15s ease; + transform: rotate(90deg); + margin-left: -3px; + margin-right: 4px; + + &.is-collapsed { + transform: rotate(0deg); + } +} diff --git a/packages/core/src/components/editor/canvas/modules/layers-panel/types.ts b/packages/core/src/components/editor/canvas/modules/layers-panel/types.ts new file mode 100644 index 0000000..7c23dbe --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/layers-panel/types.ts @@ -0,0 +1,19 @@ +export type PaletteLayerKind = 'group' | 'component' +export type PaletteLayerVisibilityState = 'visible' | 'hidden' | 'mixed' +export type PaletteLayerLockState = 'locked' | 'unlocked' | 'mixed' + +export interface PaletteLayerItem { + id: string + name: string + kind?: PaletteLayerKind + groupId?: string + componentId?: string + icon?: string + selectable?: boolean + level?: number + locked?: boolean + visible?: boolean + visibilityState?: PaletteLayerVisibilityState + lockState?: PaletteLayerLockState + hasLockedDescendants?: boolean +} diff --git a/packages/core/src/components/editor/canvas/modules/layers-panel/utils.ts b/packages/core/src/components/editor/canvas/modules/layers-panel/utils.ts new file mode 100644 index 0000000..e057b86 --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/layers-panel/utils.ts @@ -0,0 +1,144 @@ +import type { Layer } from '../../types' +import type { PaletteLayerItem } from './types' +import { asRecord, getLayerGroupMeta } from '../../grouping' + +function readStringValue(source: Record | null, keys: string[]): string | null { + if (!source) { + return null + } + + for (const key of keys) { + const value = source[key] + if (typeof value === 'string') { + const normalized = value.trim() + if (normalized) { + return normalized + } + } + } + + return null +} + +export function resolveLayerName(layer: Layer, index: number): string { + const layerRecord = asRecord(layer as unknown) + const configRecord = asRecord(layer.config) + + const directName = readStringValue(layerRecord, ['name', 'title', 'label']) + || readStringValue(configRecord, ['name', 'title', 'label']) + if (directName) { + return directName + } + + const typeLabel: Record = { + rect: '矩形', + number: '数值', + text: '文本', + bar: '棒图', + progress: '进度条', + button: '按钮', + custom: '组件', + } + + return `${typeLabel[layer.type] || layer.type}-${index + 1}` +} + +export function resolveComponentIcon(type: string) { + const iconMap: Record = { + rect: 'fa-regular fa-square', + number: 'fa-solid fa-hashtag', + text: 'fa-solid fa-font', + bar: 'fa-solid fa-mattress-pillow', + progress: 'fa-solid fa-gauge', + button: 'fa-regular fa-hand-pointer', + custom: 'fa-regular fa-square', + } + return iconMap[type] || 'fa-regular fa-square' +} + +export const resolveGroupMeta = getLayerGroupMeta + +/** 按位号标签分组图层 */ +export function groupLayersByTag( + layers: Layer[], + labelResolver?: (tagNumber: string) => string | undefined, +): { prefix: string, items: Layer[] }[] { + const groups = new Map() + const unassigned: Layer[] = [] + + for (const layer of layers) { + const tag = layer.tagNumber + if (!tag) { + unassigned.push(layer) + continue + } + const groupKey = labelResolver?.(tag) || tag + if (!groups.has(groupKey)) + groups.set(groupKey, []) + groups.get(groupKey)!.push(layer) + } + + const sortedEntries = [...groups.entries()] + sortedEntries.sort(([a], [b]) => a.localeCompare(b)) + const result = sortedEntries.map(([prefix, items]) => ({ prefix, items })) + + if (unassigned.length > 0) { + result.push({ prefix: '未分配', items: unassigned }) + } + + return result +} + +/** 按位号关键词过滤图层 */ +export function filterLayersByTag(layers: Layer[], keyword: string): Layer[] { + if (!keyword.trim()) + return layers + const kw = keyword.toLowerCase() + return layers.filter(l => l.tagNumber?.toLowerCase().includes(kw)) +} + +export function filterLayers(layers: PaletteLayerItem[], keyword: string): PaletteLayerItem[] { + const normalized = keyword.trim().toLowerCase() + if (!normalized) { + return layers + } + + const result: PaletteLayerItem[] = [] + let index = 0 + + while (index < layers.length) { + const current = layers[index] + const currentLevel = current.level || 0 + + if (current.kind !== 'group') { + if (current.name.toLowerCase().includes(normalized)) { + result.push(current) + } + index += 1 + continue + } + + const children: PaletteLayerItem[] = [] + let cursor = index + 1 + while (cursor < layers.length) { + const next = layers[cursor] + if ((next.level || 0) <= currentLevel) { + break + } + children.push(next) + cursor += 1 + } + + const matchedChildren = children.filter(child => child.name.toLowerCase().includes(normalized)) + const groupMatched = current.name.toLowerCase().includes(normalized) + + if (groupMatched || matchedChildren.length > 0) { + result.push(current) + result.push(...(groupMatched ? children : matchedChildren)) + } + + index = cursor + } + + return result +} diff --git a/packages/core/src/components/editor/canvas/modules/minimap/index.vue b/packages/core/src/components/editor/canvas/modules/minimap/index.vue new file mode 100644 index 0000000..15a900f --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/minimap/index.vue @@ -0,0 +1,366 @@ + + + + + diff --git a/packages/core/src/components/editor/canvas/modules/property-panel/composables/usePropertyContext.ts b/packages/core/src/components/editor/canvas/modules/property-panel/composables/usePropertyContext.ts new file mode 100644 index 0000000..aa55be6 --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/property-panel/composables/usePropertyContext.ts @@ -0,0 +1,27 @@ +import { inject, provide } from 'vue' + +export interface PropertyContext { + /** 更新属性字段(自动处理分组/多选) */ + updateField: (field: string, value: unknown) => void + /** 获取字段状态(自动处理分组混合值) */ + getFieldState: (fieldKey: string) => { value: unknown, mixed: boolean } +} + +// 使用字符串 key 确保 HMR 热更新时 provide/inject 不会断链 +const KEY = 'ed-property-context' + +export function providePropertyContext(ctx: PropertyContext) { + provide(KEY, ctx) +} + +export function usePropertyContext(): PropertyContext { + const ctx = inject(KEY) + if (!ctx) { + console.warn('[usePropertyContext] inject 失败,请确保在 PropertyPanel 内使用') + return { + updateField: () => {}, + getFieldState: () => ({ value: undefined, mixed: false }), + } + } + return ctx +} diff --git a/packages/core/src/components/editor/canvas/modules/property-panel/composables/usePropertyPanel.ts b/packages/core/src/components/editor/canvas/modules/property-panel/composables/usePropertyPanel.ts new file mode 100644 index 0000000..438902a --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/property-panel/composables/usePropertyPanel.ts @@ -0,0 +1,299 @@ +import { ref, shallowRef, watch } from 'vue' +import { MAX_CANVAS_SIZE, MIN_CANVAS_HEIGHT, MIN_CANVAS_WIDTH } from '@/constants' +import { useCanvasState } from '../../../context/state' +import { listenEditorEmitter, setEditorEmitter } from '../../../emitter' +import { buildGroupRectUpdates } from '../../../grouping' +import { clamp } from '../../../utils' + +export function usePropertyPanel() { + const { canvasId, canvasView, selectedLayerId, selectedLayerIds, selectedLayer, selectedGroup, isBaseLayerActive } = useCanvasState() + + const panelRef = shallowRef(null) + const visible = ref(false) + const interactionSuspended = ref(false) + const restoreVisibleAfterInteraction = ref(false) + const shouldRenderPanel = ref(Boolean(canvasId.value)) + + function normalizeLockedSizeByWidth(width: number, ratio: number) { + let nextWidth = clamp(Math.round(width), MIN_CANVAS_WIDTH, MAX_CANVAS_SIZE) + let nextHeight = Math.round(nextWidth / ratio) + + if (nextHeight < MIN_CANVAS_HEIGHT) { + nextHeight = MIN_CANVAS_HEIGHT + nextWidth = Math.round(nextHeight * ratio) + } + if (nextWidth < MIN_CANVAS_WIDTH) { + nextWidth = MIN_CANVAS_WIDTH + nextHeight = Math.round(nextWidth / ratio) + } + if (nextHeight > MAX_CANVAS_SIZE) { + nextHeight = MAX_CANVAS_SIZE + nextWidth = Math.round(nextHeight * ratio) + } + if (nextWidth > MAX_CANVAS_SIZE) { + nextWidth = MAX_CANVAS_SIZE + nextHeight = Math.round(nextWidth / ratio) + } + + return { + nextWidth: clamp(nextWidth, MIN_CANVAS_WIDTH, MAX_CANVAS_SIZE), + nextHeight: clamp(nextHeight, MIN_CANVAS_HEIGHT, MAX_CANVAS_SIZE), + } + } + + function normalizeLockedSizeByHeight(height: number, ratio: number) { + let nextHeight = clamp(Math.round(height), MIN_CANVAS_HEIGHT, MAX_CANVAS_SIZE) + let nextWidth = Math.round(nextHeight * ratio) + + if (nextWidth < MIN_CANVAS_WIDTH) { + nextWidth = MIN_CANVAS_WIDTH + nextHeight = Math.round(nextWidth / ratio) + } + if (nextHeight < MIN_CANVAS_HEIGHT) { + nextHeight = MIN_CANVAS_HEIGHT + nextWidth = Math.round(nextHeight * ratio) + } + if (nextWidth > MAX_CANVAS_SIZE) { + nextWidth = MAX_CANVAS_SIZE + nextHeight = Math.round(nextWidth / ratio) + } + if (nextHeight > MAX_CANVAS_SIZE) { + nextHeight = MAX_CANVAS_SIZE + nextWidth = Math.round(nextHeight * ratio) + } + + return { + nextWidth: clamp(nextWidth, MIN_CANVAS_WIDTH, MAX_CANVAS_SIZE), + nextHeight: clamp(nextHeight, MIN_CANVAS_HEIGHT, MAX_CANVAS_SIZE), + } + } + + function onPropertyUpdate(payload: { field: string, value: any }) { + const layer = selectedLayer.value + const group = selectedGroup.value + const { field, value } = payload + + // 多选批量编辑(非分组) + if (!layer && !group && selectedLayerIds.value.length > 1) { + if (['x', 'y', 'width', 'height'].includes(field)) { + // 位置/尺寸批量编辑暂不支持(各图层位置不同,需要分组操作) + return + } + setEditorEmitter('layer:updateFields', { + ids: selectedLayerIds.value, + field, + value, + }) + return + } + + if (!layer && !group) { + if (field === 'view.background') { + if (typeof value === 'string' && value.trim()) { + canvasView.value.background = value + } + return + } + + if (field === 'view.backgroundLockAspectRatio') { + canvasView.value.backgroundLockAspectRatio = Boolean(value) + return + } + + if (field === 'view.imageWidth' || field === 'view.imageHeight') { + const numericValue = Number(value) + if (!Number.isFinite(numericValue)) { + return + } + + const currentWidth = Math.max(MIN_CANVAS_WIDTH, Math.round(canvasView.value.imageWidth || MIN_CANVAS_WIDTH)) + const currentHeight = Math.max(MIN_CANVAS_HEIGHT, Math.round(canvasView.value.imageHeight || MIN_CANVAS_HEIGHT)) + let nextWidth = currentWidth + let nextHeight = currentHeight + + if (canvasView.value.backgroundLockAspectRatio && currentWidth > 0 && currentHeight > 0) { + const ratio = currentWidth / currentHeight + if (field === 'view.imageWidth') { + const normalized = normalizeLockedSizeByWidth(numericValue, ratio) + nextWidth = normalized.nextWidth + nextHeight = normalized.nextHeight + } + else { + const normalized = normalizeLockedSizeByHeight(numericValue, ratio) + nextWidth = normalized.nextWidth + nextHeight = normalized.nextHeight + } + } + else if (field === 'view.imageWidth') { + nextWidth = clamp(Math.round(numericValue), MIN_CANVAS_WIDTH, MAX_CANVAS_SIZE) + } + else { + nextHeight = clamp(Math.round(numericValue), MIN_CANVAS_HEIGHT, MAX_CANVAS_SIZE) + } + + setEditorEmitter('baseLayer:setSize', { width: nextWidth, height: nextHeight }) + } + return + } + + const hasCanvasSize = canvasView.value.imageWidth > 0 && canvasView.value.imageHeight > 0 + + if (group) { + if (['x', 'y', 'width', 'height'].includes(field)) { + if (!hasCanvasSize) { + return + } + const minSize = 24 + let nextX = group.x + let nextY = group.y + let nextWidth = group.width + let nextHeight = group.height + + if (field === 'x') { + nextX = clamp(value, 0, Math.max(0, canvasView.value.imageWidth - nextWidth)) + } + if (field === 'y') { + nextY = clamp(value, 0, Math.max(0, canvasView.value.imageHeight - nextHeight)) + } + if (field === 'width') { + nextWidth = clamp(value, minSize, canvasView.value.imageWidth) + nextX = clamp(nextX, 0, Math.max(0, canvasView.value.imageWidth - nextWidth)) + } + if (field === 'height') { + nextHeight = clamp(value, minSize, canvasView.value.imageHeight) + nextY = clamp(nextY, 0, Math.max(0, canvasView.value.imageHeight - nextHeight)) + } + + setEditorEmitter('layer:updateRects', buildGroupRectUpdates( + group.layers, + group, + { + x: nextX, + y: nextY, + width: nextWidth, + height: nextHeight, + }, + )) + return + } + + setEditorEmitter('layer:updateFields', { + ids: group.layerIds, + field, + value, + }) + return + } + + if (!layer) { + return + } + + if (['x', 'y', 'width', 'height'].includes(field)) { + if (!hasCanvasSize) { + return + } + const minSize = 24 + let nextX = layer.x + let nextY = layer.y + let nextWidth = layer.width + let nextHeight = layer.height + + if (field === 'x') { + nextX = clamp(value, 0, Math.max(0, canvasView.value.imageWidth - nextWidth)) + } + if (field === 'y') { + nextY = clamp(value, 0, Math.max(0, canvasView.value.imageHeight - nextHeight)) + } + if (field === 'width') { + nextWidth = clamp(value, minSize, canvasView.value.imageWidth) + nextX = clamp(nextX, 0, Math.max(0, canvasView.value.imageWidth - nextWidth)) + } + if (field === 'height') { + nextHeight = clamp(value, minSize, canvasView.value.imageHeight) + nextY = clamp(nextY, 0, Math.max(0, canvasView.value.imageHeight - nextHeight)) + } + + setEditorEmitter('layer:updateRect', { + id: layer.id, + nextX, + nextY, + nextWidth, + nextHeight, + }) + + return + } + + setEditorEmitter('layer:updateField', { + id: layer.id, + field, + value, + }) + } + + function canShowPanel() { + return Boolean(canvasId.value) + } + + function suspendPanel() { + if (!interactionSuspended.value) { + restoreVisibleAfterInteraction.value = visible.value + } + interactionSuspended.value = true + visible.value = false + } + listenEditorEmitter('propertyPanel:suspend', suspendPanel) + + function resumePanel() { + if (!interactionSuspended.value) { + return + } + + interactionSuspended.value = false + const shouldRestore = restoreVisibleAfterInteraction.value + restoreVisibleAfterInteraction.value = false + + if (shouldRestore && canShowPanel()) { + visible.value = true + } + } + listenEditorEmitter('propertyPanel:resume', resumePanel) + + function closePanel() { + restoreVisibleAfterInteraction.value = false + visible.value = canShowPanel() + } + listenEditorEmitter('propertyPanel:closePanel', closePanel) + + function openPanel() { + if (interactionSuspended.value) { + restoreVisibleAfterInteraction.value = canShowPanel() + return + } + + visible.value = canShowPanel() + } + listenEditorEmitter('propertyPanel:openPanel', openPanel) + + watch( + () => [canvasId.value, selectedLayerId.value, selectedLayerIds.value.length, selectedGroup.value?.groupId ?? '', isBaseLayerActive.value], + () => { + shouldRenderPanel.value = Boolean(canvasId.value) + if (!interactionSuspended.value) { + visible.value = canShowPanel() + } + }, + { flush: 'sync' }, + ) + + shouldRenderPanel.value = Boolean(canvasId.value) + visible.value = canShowPanel() + + return { + panelRef, + visible, + shouldRenderPanel, + onPropertyUpdate, + } +} diff --git a/packages/core/src/components/editor/canvas/modules/property-panel/composables/usePropertyPanelDock.ts b/packages/core/src/components/editor/canvas/modules/property-panel/composables/usePropertyPanelDock.ts new file mode 100644 index 0000000..6e6ea55 --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/property-panel/composables/usePropertyPanelDock.ts @@ -0,0 +1,175 @@ +import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue' + +interface PanelPosition { + x: number + y: number +} + +const STORAGE_KEY = 'ed:property-panel-dock' +const DEFAULT_POSITION: PanelPosition = { x: -1, y: -1 } +const PANEL_WIDTH = 240 +const PANEL_MIN_VISIBLE = 48 + +function loadPersistedState() { + try { + const raw = sessionStorage.getItem(STORAGE_KEY) + if (!raw) { + return null + } + const parsed = JSON.parse(raw) as { isDocked: boolean, position: PanelPosition } + if (typeof parsed.isDocked !== 'boolean') { + return null + } + return parsed + } + catch { + return null + } +} + +function persistState(isDocked: boolean, position: PanelPosition) { + try { + sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ isDocked, position })) + } + catch { + // sessionStorage 不可用时静默忽略 + } +} + +export function usePropertyPanelDock() { + const persisted = loadPersistedState() + + const isDocked = ref(persisted?.isDocked ?? true) + const position = reactive( + persisted?.position ?? { ...DEFAULT_POSITION }, + ) + + // 拖拽状态 + const isDragging = ref(false) + let dragStartX = 0 + let dragStartY = 0 + let dragStartPosX = 0 + let dragStartPosY = 0 + + // 初始化浮动位置(未设置过时居中偏右) + function initDefaultPosition() { + if (position.x < 0 || position.y < 0) { + position.x = Math.max(0, window.innerWidth - PANEL_WIDTH - 60) + position.y = 80 + } + } + + // 将面板约束在视口内 + function clampToViewport() { + const maxX = window.innerWidth - PANEL_MIN_VISIBLE + const maxY = window.innerHeight - PANEL_MIN_VISIBLE + position.x = Math.max(0, Math.min(position.x, maxX)) + position.y = Math.max(0, Math.min(position.y, maxY)) + } + + function toggleDock() { + isDocked.value = !isDocked.value + if (!isDocked.value) { + initDefaultPosition() + clampToViewport() + } + persistState(isDocked.value, position) + } + + // 拖拽处理 + function onDragStart(event: MouseEvent) { + if (isDocked.value) { + return + } + // 仅左键触发 + if (event.button !== 0) { + return + } + event.preventDefault() + isDragging.value = true + dragStartX = event.clientX + dragStartY = event.clientY + dragStartPosX = position.x + dragStartPosY = position.y + + document.addEventListener('mousemove', onDragMove) + document.addEventListener('mouseup', onDragEnd) + } + + function onDragMove(event: MouseEvent) { + if (!isDragging.value) { + return + } + const dx = event.clientX - dragStartX + const dy = event.clientY - dragStartY + position.x = dragStartPosX + dx + position.y = dragStartPosY + dy + } + + function onDragEnd() { + if (!isDragging.value) { + return + } + isDragging.value = false + clampToViewport() + persistState(isDocked.value, position) + + document.removeEventListener('mousemove', onDragMove) + document.removeEventListener('mouseup', onDragEnd) + } + + // 浮动时的样式 + const floatingStyle = computed(() => { + if (isDocked.value) { + return undefined + } + return { + position: 'fixed' as const, + left: `${position.x}px`, + top: `${position.y}px`, + zIndex: 1000, + height: 'auto', + maxHeight: '80vh', + width: `${PANEL_WIDTH}px`, + boxShadow: 'var(--ed-shadow-lg)', + borderRadius: 'var(--ed-radius-lg)', + transition: isDragging.value ? 'none' : undefined, + } + }) + + // 窗口 resize 时约束面板位置 + function onWindowResize() { + if (!isDocked.value) { + clampToViewport() + } + } + + onMounted(() => { + window.addEventListener('resize', onWindowResize) + if (!isDocked.value) { + initDefaultPosition() + clampToViewport() + } + }) + + onBeforeUnmount(() => { + window.removeEventListener('resize', onWindowResize) + document.removeEventListener('mousemove', onDragMove) + document.removeEventListener('mouseup', onDragEnd) + }) + + // 状态变化时持久化 + watch( + () => isDocked.value, + () => persistState(isDocked.value, position), + ) + + return { + isDocked, + isDragging, + position, + floatingStyle, + toggleDock, + onDragStart, + } +} diff --git a/packages/core/src/components/editor/canvas/modules/property-panel/composables/usePropertySections.ts b/packages/core/src/components/editor/canvas/modules/property-panel/composables/usePropertySections.ts new file mode 100644 index 0000000..288091d --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/property-panel/composables/usePropertySections.ts @@ -0,0 +1,127 @@ +import type { Component } from 'vue' +import { computed } from 'vue' +import { useCanvasState } from '../../../context/state' +import BarConfig from '../sections/BarConfig.vue' +import ButtonConfig from '../sections/ButtonConfig.vue' +import CanvasSwitcherConfig from '../sections/CanvasSwitcherConfig.vue' +import EventEditor from '../sections/EventEditor.vue' +import PidControllerConfig from '../sections/PidControllerConfig.vue' +import SectionFill from '../sections/SectionFill.vue' +import SectionGeometry from '../sections/SectionGeometry.vue' +import SectionNumberFormat from '../sections/SectionNumberFormat.vue' +import SectionShadow from '../sections/SectionShadow.vue' +import SectionStroke from '../sections/SectionStroke.vue' +import SectionText from '../sections/SectionText.vue' +import SectionTextContent from '../sections/SectionTextContent.vue' +import ValveControllerConfig from '../sections/ValveControllerConfig.vue' + +export interface SectionEntry { + key: string + component: Component + defaultCollapsed?: boolean +} + +const SECTION_MAP: Record = { + rect: [ + { key: 'geometry', component: SectionGeometry }, + { key: 'fill', component: SectionFill }, + { key: 'stroke', component: SectionStroke, defaultCollapsed: true }, + { key: 'shadow', component: SectionShadow, defaultCollapsed: true }, + + { key: 'event', component: EventEditor, defaultCollapsed: true }, + ], + number: [ + { key: 'geometry', component: SectionGeometry }, + { key: 'numberFormat', component: SectionNumberFormat }, + { key: 'text', component: SectionText }, + { key: 'fill', component: SectionFill, defaultCollapsed: true }, + { key: 'stroke', component: SectionStroke, defaultCollapsed: true }, + { key: 'shadow', component: SectionShadow, defaultCollapsed: true }, + { key: 'event', component: EventEditor, defaultCollapsed: true }, + ], + text: [ + { key: 'geometry', component: SectionGeometry }, + { key: 'textContent', component: SectionTextContent }, + { key: 'text', component: SectionText }, + { key: 'fill', component: SectionFill, defaultCollapsed: true }, + { key: 'stroke', component: SectionStroke, defaultCollapsed: true }, + { key: 'shadow', component: SectionShadow, defaultCollapsed: true }, + { key: 'event', component: EventEditor, defaultCollapsed: true }, + ], + bar: [ + { key: 'geometry', component: SectionGeometry }, + { key: 'barConfig', component: BarConfig }, + { key: 'fill', component: SectionFill, defaultCollapsed: true }, + { key: 'stroke', component: SectionStroke, defaultCollapsed: true }, + { key: 'event', component: EventEditor, defaultCollapsed: true }, + ], + button: [ + { key: 'geometry', component: SectionGeometry }, + { key: 'buttonConfig', component: ButtonConfig }, + { key: 'text', component: SectionText }, + { key: 'fill', component: SectionFill, defaultCollapsed: true }, + { key: 'stroke', component: SectionStroke, defaultCollapsed: true }, + { key: 'event', component: EventEditor }, + ], + pidController: [ + { key: 'geometry', component: SectionGeometry }, + { key: 'pidControllerConfig', component: PidControllerConfig }, + { key: 'stroke', component: SectionStroke, defaultCollapsed: true }, + ], + valveController: [ + { key: 'geometry', component: SectionGeometry }, + { key: 'valveControllerConfig', component: ValveControllerConfig }, + { key: 'stroke', component: SectionStroke, defaultCollapsed: true }, + ], + canvasSwitcher: [ + { key: 'geometry', component: SectionGeometry }, + { key: 'canvasSwitcherConfig', component: CanvasSwitcherConfig }, + { key: 'fill', component: SectionFill, defaultCollapsed: true }, + { key: 'stroke', component: SectionStroke, defaultCollapsed: true }, + { key: 'event', component: EventEditor, defaultCollapsed: true }, + ], +} + +const FALLBACK_SECTIONS: SectionEntry[] = [ + { key: 'geometry', component: SectionGeometry }, + { key: 'fill', component: SectionFill }, + { key: 'stroke', component: SectionStroke, defaultCollapsed: true }, + { key: 'shadow', component: SectionShadow, defaultCollapsed: true }, + { key: 'event', component: EventEditor, defaultCollapsed: true }, +] + +// 多选不同类型时使用公共 Section(geometry + fill + stroke) +const MULTI_SELECT_SECTIONS: SectionEntry[] = [ + { key: 'geometry', component: SectionGeometry }, + { key: 'fill', component: SectionFill }, + { key: 'stroke', component: SectionStroke, defaultCollapsed: true }, +] + +export function usePropertySections() { + const { selectedLayer, selectedGroup, selectedLayerIds } = useCanvasState() + + const sections = computed(() => { + // 多选(非分组)时显示公共属性面板 + if (!selectedLayer.value && !selectedGroup.value && selectedLayerIds.value.length > 1) { + return MULTI_SELECT_SECTIONS + } + + // 分组选择:按成员类型计算公共 Sections + if (selectedGroup.value) { + const types = [...new Set(selectedGroup.value.layers.map(l => l.type))] + if (types.length === 1) { + return SECTION_MAP[types[0]] ?? FALLBACK_SECTIONS + } + return MULTI_SELECT_SECTIONS + } + + if (!selectedLayer.value) { + return [] + } + + const type = selectedLayer.value.type + return SECTION_MAP[type] ?? FALLBACK_SECTIONS + }) + + return { sections } +} diff --git a/packages/core/src/components/editor/canvas/modules/property-panel/index.vue b/packages/core/src/components/editor/canvas/modules/property-panel/index.vue new file mode 100644 index 0000000..4fc24ca --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/property-panel/index.vue @@ -0,0 +1,898 @@ + + + + + diff --git a/packages/core/src/components/editor/canvas/modules/property-panel/sections/BarConfig.vue b/packages/core/src/components/editor/canvas/modules/property-panel/sections/BarConfig.vue new file mode 100644 index 0000000..e2c28f5 --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/property-panel/sections/BarConfig.vue @@ -0,0 +1,222 @@ + + + + + diff --git a/packages/core/src/components/editor/canvas/modules/property-panel/sections/BindingConfigDialog.vue b/packages/core/src/components/editor/canvas/modules/property-panel/sections/BindingConfigDialog.vue new file mode 100644 index 0000000..e539991 --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/property-panel/sections/BindingConfigDialog.vue @@ -0,0 +1,1073 @@ + + + + + diff --git a/packages/core/src/components/editor/canvas/modules/property-panel/sections/ButtonConfig.vue b/packages/core/src/components/editor/canvas/modules/property-panel/sections/ButtonConfig.vue new file mode 100644 index 0000000..f6af4ba --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/property-panel/sections/ButtonConfig.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/packages/core/src/components/editor/canvas/modules/property-panel/sections/CanvasSwitcherConfig.vue b/packages/core/src/components/editor/canvas/modules/property-panel/sections/CanvasSwitcherConfig.vue new file mode 100644 index 0000000..207f61a --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/property-panel/sections/CanvasSwitcherConfig.vue @@ -0,0 +1,105 @@ + + + + + diff --git a/packages/core/src/components/editor/canvas/modules/property-panel/sections/EventConfigDialog.vue b/packages/core/src/components/editor/canvas/modules/property-panel/sections/EventConfigDialog.vue new file mode 100644 index 0000000..0403295 --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/property-panel/sections/EventConfigDialog.vue @@ -0,0 +1,265 @@ + + + + + diff --git a/packages/core/src/components/editor/canvas/modules/property-panel/sections/EventEditor.vue b/packages/core/src/components/editor/canvas/modules/property-panel/sections/EventEditor.vue new file mode 100644 index 0000000..0024dc3 --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/property-panel/sections/EventEditor.vue @@ -0,0 +1,240 @@ + + + + + diff --git a/packages/core/src/components/editor/canvas/modules/property-panel/sections/GradientEditor.vue b/packages/core/src/components/editor/canvas/modules/property-panel/sections/GradientEditor.vue new file mode 100644 index 0000000..51d8078 --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/property-panel/sections/GradientEditor.vue @@ -0,0 +1,194 @@ + + + + + diff --git a/packages/core/src/components/editor/canvas/modules/property-panel/sections/PidControllerConfig.vue b/packages/core/src/components/editor/canvas/modules/property-panel/sections/PidControllerConfig.vue new file mode 100644 index 0000000..aa943a3 --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/property-panel/sections/PidControllerConfig.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/packages/core/src/components/editor/canvas/modules/property-panel/sections/SectionDataBinding.vue b/packages/core/src/components/editor/canvas/modules/property-panel/sections/SectionDataBinding.vue new file mode 100644 index 0000000..b52ea1a --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/property-panel/sections/SectionDataBinding.vue @@ -0,0 +1,600 @@ + + + + + diff --git a/packages/core/src/components/editor/canvas/modules/property-panel/sections/SectionFill.vue b/packages/core/src/components/editor/canvas/modules/property-panel/sections/SectionFill.vue new file mode 100644 index 0000000..6c87946 --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/property-panel/sections/SectionFill.vue @@ -0,0 +1,245 @@ + + + + + diff --git a/packages/core/src/components/editor/canvas/modules/property-panel/sections/SectionGeometry.vue b/packages/core/src/components/editor/canvas/modules/property-panel/sections/SectionGeometry.vue new file mode 100644 index 0000000..ad4b0cd --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/property-panel/sections/SectionGeometry.vue @@ -0,0 +1,181 @@ + + + + + diff --git a/packages/core/src/components/editor/canvas/modules/property-panel/sections/SectionNumberFormat.vue b/packages/core/src/components/editor/canvas/modules/property-panel/sections/SectionNumberFormat.vue new file mode 100644 index 0000000..5d66ebf --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/property-panel/sections/SectionNumberFormat.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/packages/core/src/components/editor/canvas/modules/property-panel/sections/SectionShadow.vue b/packages/core/src/components/editor/canvas/modules/property-panel/sections/SectionShadow.vue new file mode 100644 index 0000000..4ab0826 --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/property-panel/sections/SectionShadow.vue @@ -0,0 +1,129 @@ + + + + + diff --git a/packages/core/src/components/editor/canvas/modules/property-panel/sections/SectionStroke.vue b/packages/core/src/components/editor/canvas/modules/property-panel/sections/SectionStroke.vue new file mode 100644 index 0000000..b2c2e8b --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/property-panel/sections/SectionStroke.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/packages/core/src/components/editor/canvas/modules/property-panel/sections/SectionText.vue b/packages/core/src/components/editor/canvas/modules/property-panel/sections/SectionText.vue new file mode 100644 index 0000000..6f25ede --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/property-panel/sections/SectionText.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/packages/core/src/components/editor/canvas/modules/property-panel/sections/SectionTextContent.vue b/packages/core/src/components/editor/canvas/modules/property-panel/sections/SectionTextContent.vue new file mode 100644 index 0000000..a4dd4e0 --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/property-panel/sections/SectionTextContent.vue @@ -0,0 +1,21 @@ + + + diff --git a/packages/core/src/components/editor/canvas/modules/property-panel/sections/ValveControllerConfig.vue b/packages/core/src/components/editor/canvas/modules/property-panel/sections/ValveControllerConfig.vue new file mode 100644 index 0000000..1bfb25c --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/property-panel/sections/ValveControllerConfig.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/packages/core/src/components/editor/canvas/modules/shared/context-menu.vue b/packages/core/src/components/editor/canvas/modules/shared/context-menu.vue new file mode 100644 index 0000000..f952744 --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/shared/context-menu.vue @@ -0,0 +1,207 @@ + + + + + diff --git a/packages/core/src/components/editor/canvas/modules/stage/composables/canvas-stage.ts b/packages/core/src/components/editor/canvas/modules/stage/composables/canvas-stage.ts new file mode 100644 index 0000000..0f5d5b6 --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/stage/composables/canvas-stage.ts @@ -0,0 +1,703 @@ +import { computed, nextTick, onBeforeUnmount, onMounted, reactive, shallowRef, watch } from 'vue' +import { getCanvasByIdApi } from '@/api' +import { MAX_CANVAS_SIZE, MIN_CANVAS_HEIGHT, MIN_CANVAS_WIDTH } from '@/constants' +import pinia, { useCanvasStore, useLayerStore } from '@/stores' +import { cloneData, extractLayers } from '@/utils' +import { useLayerSelection } from '../../../composables/layer' +import { useLayerPersistence } from '../../../composables/persistence' +import { useCanvasInject, useCanvasRuntimeInject, useEditorUIInject } from '../../../context' +import { useCanvasState } from '../../../context/state' +import { listenEditorEmitter, setEditorEmitter } from '../../../emitter' +import { unwrapElement } from '../../../utils' + +export function useCanvasStage() { + // 这里同时负责“底图渲染”和“视口控制”,因此常量集中定义在入口。 + const DEFAULT_CANVAS_WIDTH_RATIO = 0.6 + const STAGE_BACKGROUND = '#EEF1F5' + const DEFAULT_BASE_LAYER_BACKGROUND = '#FFFFFF' + const MIN_VIEW_SCALE = 0.1 + const MAX_VIEW_SCALE = 100 + + const { stageRef, getCanvasElement, getWrapperElement } = useCanvasInject() + const { setBackgroundAverageColor } = useCanvasRuntimeInject() + const layerStore = useLayerStore(pinia) + const { clipboard } = useEditorUIInject() + + const imageRef = shallowRef(null) + + const { canvasId, canvasView, activeCanvasThumbnail, isHydratingCanvas, canvasInfo } = useCanvasState() + + let resizeObserver: ResizeObserver | null = null + let observedWrapper: HTMLElement | null = null + // 平移使用屏幕坐标记录起点,再映射回当前视口偏移。 + const panState = reactive({ + active: false, + startClientX: 0, + startClientY: 0, + startOffsetX: 0, + startOffsetY: 0, + }) + + const wrapperStyle = computed(() => ({ + backgroundColor: STAGE_BACKGROUND, + })) + + function updateCanvasSize() { + const wrapper = getWrapperElement() + const canvas = getCanvasElement() + if (!wrapper || !canvas) { + return + } + const rect = wrapper.getBoundingClientRect() + const width = Math.max(1, Math.floor(rect.width)) + const height = Math.max(1, Math.floor(rect.height)) + if (canvas.width !== width) { + canvas.width = width + } + if (canvas.height !== height) { + canvas.height = height + } + canvasView.value.canvasWidth = width + canvasView.value.canvasHeight = height + updateLayout() + } + + function getDefaultScale() { + // 默认缩放优先保证宽度可见,再受高度约束,避免初次进入时铺满过头。 + if (!canvasView.value.imageWidth || !canvasView.value.imageHeight || !canvasView.value.canvasWidth || !canvasView.value.canvasHeight) { + return 1 + } + const targetScaleByWidth = (canvasView.value.canvasWidth * DEFAULT_CANVAS_WIDTH_RATIO) / canvasView.value.imageWidth + const maxScaleByHeight = canvasView.value.canvasHeight / canvasView.value.imageHeight + const scale = Math.min(targetScaleByWidth, maxScaleByHeight) + return Number.isFinite(scale) && scale > 0 ? scale : 1 + } + + function centerBaseLayer() { + const drawWidth = canvasView.value.imageWidth * canvasView.value.scale + const drawHeight = canvasView.value.imageHeight * canvasView.value.scale + canvasView.value.offsetX = Math.round((canvasView.value.canvasWidth - drawWidth) / 2) + canvasView.value.offsetY = Math.round((canvasView.value.canvasHeight - drawHeight) / 2) + } + + function resetViewToDefault() { + if (!getCanvasElement()) { + return + } + if (!canvasView.value.imageWidth || !canvasView.value.imageHeight || !canvasView.value.canvasWidth || !canvasView.value.canvasHeight) { + canvasView.value.scale = 1 + canvasView.value.offsetX = 0 + canvasView.value.offsetY = 0 + drawCanvas() + return + } + canvasView.value.scale = getDefaultScale() + centerBaseLayer() + drawCanvas() + } + + function updateLayout(options?: { resetView?: boolean }) { + if (!getCanvasElement()) { + return + } + if (!canvasView.value.imageWidth || !canvasView.value.imageHeight || !canvasView.value.canvasWidth || !canvasView.value.canvasHeight) { + // 尺寸不足时也要触发一次重绘,避免保留上一张画布残影。 + drawCanvas() + return + } + if (options?.resetView) { + resetViewToDefault() + return + } + drawCanvas() + } + + function isBaseLayerInViewport() { + if (!canvasView.value.imageWidth || !canvasView.value.imageHeight || !canvasView.value.canvasWidth || !canvasView.value.canvasHeight || canvasView.value.scale <= 0) { + return false + } + const drawWidth = canvasView.value.imageWidth * canvasView.value.scale + const drawHeight = canvasView.value.imageHeight * canvasView.value.scale + const left = canvasView.value.offsetX + const top = canvasView.value.offsetY + const right = left + drawWidth + const bottom = top + drawHeight + return right > 0 && bottom > 0 && left < canvasView.value.canvasWidth && top < canvasView.value.canvasHeight + } + + function drawCanvas() { + const canvas = getCanvasElement() + if (!canvas) { + return + } + const ctx = canvas.getContext('2d') + if (!ctx) { + return + } + ctx.clearRect(0, 0, canvas.width, canvas.height) + ctx.fillStyle = STAGE_BACKGROUND + ctx.fillRect(0, 0, canvas.width, canvas.height) + const drawWidth = canvasView.value.imageWidth * canvasView.value.scale + const drawHeight = canvasView.value.imageHeight * canvasView.value.scale + const image = imageRef.value + + // 每次重绘都先完整铺背景,避免缩放和平移后出现旧像素残留。 + if (drawWidth > 0 && drawHeight > 0) { + ctx.fillStyle = canvasView.value.background || DEFAULT_BASE_LAYER_BACKGROUND + ctx.fillRect(canvasView.value.offsetX, canvasView.value.offsetY, drawWidth, drawHeight) + if (image) { + ctx.drawImage(image, canvasView.value.offsetX, canvasView.value.offsetY, drawWidth, drawHeight) + } + else { + // 无底图时保留一个逻辑画布区域,便于后续设置宽高。 + ctx.strokeStyle = '#D0D7DE' + ctx.lineWidth = 1 + ctx.strokeRect(canvasView.value.offsetX, canvasView.value.offsetY, drawWidth, drawHeight) + } + } + } + + function sampleImageAverageColor(image: HTMLImageElement) { + // 采样缩小后的像素均值,用来给面板和文字自动选对比色。 + const width = Math.max(1, Math.min(32, image.naturalWidth || image.width || 1)) + const height = Math.max(1, Math.min(32, image.naturalHeight || image.height || 1)) + const canvas = document.createElement('canvas') + canvas.width = width + canvas.height = height + const ctx = canvas.getContext('2d', { willReadFrequently: true }) + if (!ctx) { + return null + } + + ctx.drawImage(image, 0, 0, width, height) + const imageData = ctx.getImageData(0, 0, width, height).data + let red = 0 + let green = 0 + let blue = 0 + let alphaCount = 0 + + for (let index = 0; index < imageData.length; index += 4) { + const alpha = imageData[index + 3] / 255 + if (alpha <= 0) { + continue + } + red += imageData[index] * alpha + green += imageData[index + 1] * alpha + blue += imageData[index + 2] * alpha + alphaCount += alpha + } + + if (!alphaCount) { + return null + } + + const toHex = (value: number) => Math.round(value / alphaCount).toString(16).padStart(2, '0') + return `#${toHex(red)}${toHex(green)}${toHex(blue)}` + } + + function scaleNumericValue(value: unknown, factor: number) { + if (typeof value !== 'number' || !Number.isFinite(value)) { + return value + } + return Math.max(0, Number((value * factor).toFixed(2))) + } + + function scaleLayersToBaseLayer(nextWidth: number, nextHeight: number, prevWidth: number, prevHeight: number) { + if (!layerStore.layerList.length || prevWidth <= 0 || prevHeight <= 0 || isHydratingCanvas.value) { + return + } + + const scaleX = nextWidth / prevWidth + const scaleY = nextHeight / prevHeight + if (!Number.isFinite(scaleX) || !Number.isFinite(scaleY) || (!scaleX && !scaleY)) { + return + } + if (Math.abs(scaleX - 1) < 0.0001 && Math.abs(scaleY - 1) < 0.0001) { + return + } + + const typographyScale = Math.sqrt(scaleX * scaleY) + // 组件几何属性按 X/Y 缩放,文字和阴影模糊这类视觉属性按综合比例缩放。 + const nextLayers = layerStore.layerList.map((layer) => { + const nextLayer = cloneData(layer) + nextLayer.x = Math.round(nextLayer.x * scaleX) + nextLayer.y = Math.round(nextLayer.y * scaleY) + nextLayer.width = Math.max(24, Math.round(nextLayer.width * scaleX)) + nextLayer.height = Math.max(24, Math.round(nextLayer.height * scaleY)) + + const nextConfig = typeof nextLayer.config === 'object' && nextLayer.config + ? nextLayer.config as Record + : undefined + if (nextConfig) { + nextConfig.fontSize = scaleNumericValue(nextConfig.fontSize, typographyScale) + nextConfig.strokeWidth = scaleNumericValue(nextConfig.strokeWidth, typographyScale) + nextConfig.radius = scaleNumericValue(nextConfig.radius, typographyScale) + nextConfig.shadowBlur = scaleNumericValue(nextConfig.shadowBlur, typographyScale) + nextConfig.shadowOffsetX = scaleNumericValue(nextConfig.shadowOffsetX, scaleX) + nextConfig.shadowOffsetY = scaleNumericValue(nextConfig.shadowOffsetY, scaleY) + } + + const nextStyle = typeof nextLayer.style === 'object' && nextLayer.style + ? nextLayer.style as Record + : undefined + if (nextStyle?.text) { + nextStyle.text.fontSize = scaleNumericValue(nextStyle.text.fontSize, typographyScale) + } + if (nextStyle?.border) { + nextStyle.border.width = scaleNumericValue(nextStyle.border.width, typographyScale) + nextStyle.border.radius = scaleNumericValue(nextStyle.border.radius, typographyScale) + } + if (nextStyle?.shadow) { + nextStyle.shadow.blur = scaleNumericValue(nextStyle.shadow.blur, typographyScale) + nextStyle.shadow.offsetX = scaleNumericValue(nextStyle.shadow.offsetX, scaleX) + nextStyle.shadow.offsetY = scaleNumericValue(nextStyle.shadow.offsetY, scaleY) + } + + return nextLayer + }) + + layerStore.setLayerList(nextLayers, { preserveSelection: true }) + } + + function ensureObserver() { + const wrapper = getWrapperElement() + if (!wrapper) { + return + } + // ResizeObserver 负责容器尺寸变化时重算画布尺寸。 + if (!resizeObserver) { + resizeObserver = new ResizeObserver(() => { + updateCanvasSize() + }) + } + if (observedWrapper && observedWrapper !== wrapper) { + resizeObserver.unobserve(observedWrapper) + } + if (observedWrapper !== wrapper) { + resizeObserver.observe(wrapper) + observedWrapper = wrapper + } + } + + async function loadBackground( + imageUrl: string, + options?: { width?: number, height?: number, lockAspectRatio?: boolean, resetView?: boolean }, + ): Promise { + // 底图加载完成后,同时同步自然尺寸、取样颜色和逻辑画布尺寸。 + const image = new Image() + image.src = imageUrl + + await new Promise((resolve, reject) => { + image.onload = () => resolve() + image.onerror = () => reject(new Error('底图加载失败')) + }) + + imageRef.value = image + const averageColor = sampleImageAverageColor(image) + canvasView.value.backgroundAverageColor = averageColor + setBackgroundAverageColor(averageColor) + canvasView.value.imageNaturalWidth = image.naturalWidth + canvasView.value.imageNaturalHeight = image.naturalHeight + const safeWidth = Number(options?.width) + const safeHeight = Number(options?.height) + const nextWidth = Number.isFinite(safeWidth) && safeWidth > 0 + ? Math.round(safeWidth) + : image.naturalWidth + const nextHeight = Number.isFinite(safeHeight) && safeHeight > 0 + ? Math.round(safeHeight) + : image.naturalHeight + + setBaseLayerSize({ width: nextWidth, height: nextHeight, options: { resetView: options?.resetView } }) + + if (typeof options?.lockAspectRatio === 'boolean') { + canvasView.value.backgroundLockAspectRatio = options.lockAspectRatio + } + } + + function clearBackground(width = 0, height = 0, options?: { resetView?: boolean }) { + // 清底图时保留逻辑宽高的入口,便于“纯色画布”场景继续编辑。 + imageRef.value = null + canvasView.value.backgroundAverageColor = null + setBackgroundAverageColor(null) + canvasView.value.imageNaturalWidth = 0 + canvasView.value.imageNaturalHeight = 0 + setBaseLayerSize({ width, height, options }) + } + + // 批量更新背景图层尺寸,避免分别设置宽高引起多次重排抖动。 + function setBaseLayerSize({ width, height, options }: { width: number, height: number, options?: { resetView?: boolean } }) { + const rawWidth = Math.round(width) + const rawHeight = Math.round(height) + const prevWidth = canvasView.value.imageWidth + const prevHeight = canvasView.value.imageHeight + + // 当未加载任何画布数据时允许清空尺寸,其余场景统一走画布尺寸边界。 + if (rawWidth <= 0 && rawHeight <= 0) { + canvasView.value.imageWidth = 0 + canvasView.value.imageHeight = 0 + updateLayout({ resetView: options?.resetView }) + return + } + + const nextWidth = Math.min(MAX_CANVAS_SIZE, Math.max(MIN_CANVAS_WIDTH, rawWidth)) + const nextHeight = Math.min(MAX_CANVAS_SIZE, Math.max(MIN_CANVAS_HEIGHT, rawHeight)) + scaleLayersToBaseLayer(nextWidth, nextHeight, prevWidth, prevHeight) + canvasView.value.imageWidth = nextWidth + canvasView.value.imageHeight = nextHeight + updateLayout({ resetView: options?.resetView }) + } + listenEditorEmitter('baseLayer:setSize', setBaseLayerSize) + + async function setup() { + watch( + () => stageRef.value, + (next) => { + stageRef.value = next + if (next) { + // stage 引用就绪后,立即同步一次画布尺寸。 + updateCanvasSize() + ensureObserver() + } + }, + { immediate: true }, + ) + + watch( + () => unwrapElement(stageRef.value?.wrapperRef), + (wrapper) => { + if (!wrapper) { + return + } + // wrapper DOM 就绪后,再测量一次尺寸。 + updateCanvasSize() + ensureObserver() + }, + { immediate: true }, + ) + + await nextTick() + updateCanvasSize() + ensureObserver() + requestAnimationFrame(() => { + updateCanvasSize() + }) + } + + watch( + () => unwrapElement(stageRef.value?.wrapperRef), + (wrapper) => { + if (!wrapper) { + return + } + updateCanvasSize() + }, + ) + + onBeforeUnmount(() => { + window.removeEventListener('pointermove', onPanPointerMove) + window.removeEventListener('pointerup', onPanPointerUp) + if (resizeObserver && observedWrapper) { + resizeObserver.unobserve(observedWrapper) + } + resizeObserver?.disconnect() + resizeObserver = null + observedWrapper = null + }) + + function setScale(newScale: number, anchor?: { clientX: number, clientY: number }) { + const clampedScale = Math.min(Math.max(newScale, MIN_VIEW_SCALE), MAX_VIEW_SCALE) + const prevScale = canvasView.value.scale + if (!Number.isFinite(clampedScale) || prevScale <= 0 || clampedScale === prevScale) { + return + } + + if (anchor) { + // 以鼠标所在点为缩放锚点,保证缩放前后的视觉焦点不漂移。 + const canvas = getCanvasElement() + if (canvas) { + const rect = canvas.getBoundingClientRect() + const canvasX = anchor.clientX - rect.left + const canvasY = anchor.clientY - rect.top + const imageX = (canvasX - canvasView.value.offsetX) / prevScale + const imageY = (canvasY - canvasView.value.offsetY) / prevScale + canvasView.value.scale = clampedScale + canvasView.value.offsetX = canvasX - imageX * clampedScale + canvasView.value.offsetY = canvasY - imageY * clampedScale + drawCanvas() + return + } + } + + canvasView.value.scale = clampedScale + drawCanvas() + } + + function onPanPointerMove(event: PointerEvent) { + if (!panState.active) { + return + } + canvasView.value.offsetX = panState.startOffsetX + (event.clientX - panState.startClientX) + canvasView.value.offsetY = panState.startOffsetY + (event.clientY - panState.startClientY) + drawCanvas() + } + + function onPanPointerUp() { + panState.active = false + window.removeEventListener('pointermove', onPanPointerMove) + window.removeEventListener('pointerup', onPanPointerUp) + setEditorEmitter('propertyPanel:resume') + } + + function beginPan(event: PointerEvent) { + if (!getCanvasElement()) { + return false + } + event.preventDefault() + // 拖动画布期间暂时隐藏属性面板,避免遮挡和跟随重算。 + setEditorEmitter('propertyPanel:suspend') + panState.active = true + panState.startClientX = event.clientX + panState.startClientY = event.clientY + panState.startOffsetX = canvasView.value.offsetX + panState.startOffsetY = canvasView.value.offsetY + window.addEventListener('pointermove', onPanPointerMove) + window.addEventListener('pointerup', onPanPointerUp) + return true + } + + function panBy(deltaX: number, deltaY: number) { + if (!Number.isFinite(deltaX) || !Number.isFinite(deltaY)) { + return + } + if (!deltaX && !deltaY) { + return + } + canvasView.value.offsetX += deltaX + canvasView.value.offsetY += deltaY + drawCanvas() + } + + function centerTarget(bounds: { x: number, y: number, width: number, height: number }) { + if (!Number.isFinite(bounds.x) || !Number.isFinite(bounds.y) || !Number.isFinite(bounds.width) || !Number.isFinite(bounds.height)) { + return + } + if (!canvasView.value.canvasWidth || !canvasView.value.canvasHeight || canvasView.value.scale <= 0) { + return + } + + const targetCenterX = bounds.x + bounds.width / 2 + const targetCenterY = bounds.y + bounds.height / 2 + canvasView.value.offsetX = Math.round(canvasView.value.canvasWidth / 2 - targetCenterX * canvasView.value.scale) + canvasView.value.offsetY = Math.round(canvasView.value.canvasHeight / 2 - targetCenterY * canvasView.value.scale) + drawCanvas() + } + + listenEditorEmitter('viewport:setScale', ({ newScale, anchor }) => { + setScale(newScale, anchor) + }) + + listenEditorEmitter('viewport:panBy', ({ deltaX, deltaY }) => { + panBy(deltaX, deltaY) + }) + + listenEditorEmitter('viewport:beginPan', (event) => { + beginPan(event) + }) + + listenEditorEmitter('viewport:centerTarget', (bounds) => { + centerTarget(bounds) + }) + + function zoomIn() { + setScale(canvasView.value.scale * 1.1) + } + + function zoomOut() { + setScale(canvasView.value.scale / 1.1) + } + + function resetZoom() { + resetViewToDefault() + } + + function resetCanvasPresentation(options?: { resetView?: boolean }) { + useLayerSelection().replaceLayers([]) + layerStore.deactivateBaseLayerSelection() + clipboard.resetClipboard() + activeCanvasThumbnail.value = '' + canvasInfo.value = null + canvasView.value.backgroundLockAspectRatio = true + canvasView.value.background = DEFAULT_BASE_LAYER_BACKGROUND + canvasView.value.backgroundAverageColor = null + setBackgroundAverageColor(null) + clearBackground(0, 0, { resetView: options?.resetView }) + } + + // 按画布 ID 从 request 层加载组件和底图 + async function hydrateCanvasData(canvasId: string, hydrateOptions?: { forceResetView?: boolean }) { + // 恢复画布时以服务端详情为真值,统一重建底图、尺寸和组件列表。 + const detail = canvasInfo.value?.id === canvasId + ? canvasInfo.value + : await getCanvasByIdApi(canvasId) + canvasInfo.value = detail + + if (!detail) { + resetCanvasPresentation({ resetView: hydrateOptions?.forceResetView ?? true }) + return + } + + const nextLayers = extractLayers(detail.components as unknown[]) + const backgroundConfig = detail.config?.style?.background + const lockAspectRatio = typeof backgroundConfig?.lockAspectRatio === 'boolean' + ? backgroundConfig.lockAspectRatio + : true + const backgroundColor = typeof backgroundConfig?.color === 'string' && backgroundConfig.color.trim() + ? backgroundConfig.color.trim() + : '#FFFFFF' + const hasConfiguredBackgroundSize = typeof backgroundConfig?.width === 'number' + && backgroundConfig.width > 0 + && typeof backgroundConfig?.height === 'number' + && backgroundConfig.height > 0 + const nextImageWidth = Math.max(MIN_CANVAS_WIDTH, hasConfiguredBackgroundSize ? backgroundConfig.width! : detail.width!) + const nextImageHeight = Math.max(MIN_CANVAS_HEIGHT, hasConfiguredBackgroundSize ? backgroundConfig.height! : detail.height!) + + isHydratingCanvas.value = true + try { + // 切换画布时先完成底图更新,再一次性替换组件,减少中间态闪烁。 + const nextThumbnail = detail.thumbnail || '' + activeCanvasThumbnail.value = nextThumbnail + canvasView.value.backgroundLockAspectRatio = lockAspectRatio + canvasView.value.background = backgroundColor + setBackgroundAverageColor(null) + if (detail.thumbnail) { + await loadBackground(detail.thumbnail, { + width: hasConfiguredBackgroundSize ? nextImageWidth : undefined, + height: hasConfiguredBackgroundSize ? nextImageHeight : undefined, + lockAspectRatio, + }) + if (canvasView.value.imageWidth < MIN_CANVAS_WIDTH || canvasView.value.imageHeight < MIN_CANVAS_HEIGHT) { + setEditorEmitter('baseLayer:setSize', { + width: Math.max(MIN_CANVAS_WIDTH, canvasView.value.imageWidth), + height: Math.max(MIN_CANVAS_HEIGHT, canvasView.value.imageHeight), + }) + } + } + else { + clearBackground(nextImageWidth, nextImageHeight) + } + useLayerSelection().replaceLayers(nextLayers) + clipboard.resetClipboard() + if (hydrateOptions?.forceResetView || !isBaseLayerInViewport()) { + resetViewToDefault() + } + } + finally { + isHydratingCanvas.value = false + } + } + + onMounted(() => { + setup() + + if (!canvasId.value) { + resetCanvasPresentation({ resetView: true }) + return + } + // 首次进入编辑器时恢复画布 + hydrateCanvasData(canvasId.value, { forceResetView: true }) + }) + + const canvasStore = useCanvasStore(pinia) + watch( + () => canvasId.value, + async (nextCanvasId, prevCanvasId) => { + if (!nextCanvasId) { + resetCanvasPresentation({ resetView: true }) + return + } + if (nextCanvasId === prevCanvasId) { + return + } + // 切换画布时重新恢复数据 + await hydrateCanvasData(nextCanvasId) + }, + ) + + watch( + () => canvasView.value.scale, + (scale) => { + const zoom = Math.max(1, Math.round(scale * 100)) + canvasStore.setCanvasZoom(zoom) + }, + { immediate: true }, + ) + + const { persistCanvasBaseLayer } = useLayerPersistence() + watch( + () => [canvasView.value.imageWidth, canvasView.value.imageHeight, canvasView.value.backgroundLockAspectRatio, canvasView.value.background], + () => { + // 底图尺寸、背景色或宽高比锁定状态变化后统一自动保存。 + persistCanvasBaseLayer() + }, + ) + + async function importBackground(source: string) { + activeCanvasThumbnail.value = source + try { + await loadBackground(source, { + lockAspectRatio: canvasView.value.backgroundLockAspectRatio, + }) + if (canvasView.value.imageWidth < MIN_CANVAS_WIDTH || canvasView.value.imageHeight < MIN_CANVAS_HEIGHT) { + setBaseLayerSize( + { width: Math.max(MIN_CANVAS_WIDTH, canvasView.value.imageWidth), height: Math.max(MIN_CANVAS_HEIGHT, canvasView.value.imageHeight) }, + ) + } + persistCanvasBaseLayer() + } + catch (error) { + console.error('[canvas] failed to import background', error) + } + } + + async function onReplaceBackground(payload: { source: string }) { + if (!payload.source) { + return + } + await importBackground(payload.source) + layerStore.activateBaseLayerSelection() + } + listenEditorEmitter('propertyPanel:replaceBackground', onReplaceBackground) + + function onRemoveBackground() { + if (!canvasView.value.imageWidth || !canvasView.value.imageHeight) { + return + } + activeCanvasThumbnail.value = '' + clearBackground(canvasView.value.imageWidth, canvasView.value.imageHeight) + persistCanvasBaseLayer() + layerStore.activateBaseLayerSelection() + } + listenEditorEmitter('propertyPanel:removeBackground', onRemoveBackground) + + return { + setup, + canvasView, + wrapperStyle, + updateCanvasSize, + updateLayout, + getWrapperElement, + getCanvasElement, + zoomIn, + zoomOut, + resetZoom, + resetViewToDefault, + isBaseLayerInViewport, + setScale, + beginPan, + panBy, + loadBackground, + clearBackground, + setBaseLayerSize, + hydrateCanvasData, + } +} diff --git a/packages/core/src/components/editor/canvas/modules/stage/composables/useLayerInteraction.ts b/packages/core/src/components/editor/canvas/modules/stage/composables/useLayerInteraction.ts new file mode 100644 index 0000000..50042a2 --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/stage/composables/useLayerInteraction.ts @@ -0,0 +1,525 @@ +import type { Layer, ResizeHandle } from '../../../types' +import type { Rect } from './useSnapGuides' +import { onBeforeUnmount, reactive } from 'vue' +import { useLayerStore } from '@/stores' +import { useCanvasInject } from '../../../context' +import { useCanvasState } from '../../../context/state' +import { listenEditorEmitter, setEditorEmitter } from '../../../emitter' +import { + buildGroupPositionUpdates, + buildGroupRectUpdates, + getLayerGroupById, +} from '../../../grouping' +import { clamp } from '../../../utils' +import { useSnapGuides } from './useSnapGuides' + +interface LayerRectSnapshot { + id: string + x: number + y: number + width: number + height: number +} + +export function useLayerInteraction() { + const { canvasView, layerList, selectedLayerId, selectedLayerIds } = useCanvasState() + const { getCanvasElement } = useCanvasInject() + const layerStore = useLayerStore() + const { snapGuides, updateGuides, clearGuides, calcSnap, calcResizeSnap } = useSnapGuides() + + const dragState = reactive({ + active: false, + ids: [] as string[], + groupId: null as string | null, + startPointerX: 0, + startPointerY: 0, + startRects: new Map(), + startGroupBounds: null as Omit | null, + }) + + const resizeState = reactive({ + id: null as string | null, + groupId: null as string | null, + handle: null as ResizeHandle | null, + startX: 0, + startY: 0, + startLeft: 0, + startTop: 0, + startRight: 0, + startBottom: 0, + startRects: new Map(), + }) + + function getPointerImagePosition(event: PointerEvent) { + // 舞台交互全部在“底图坐标系”里计算,避免受缩放和平移影响。 + const rect = getCanvasElement()?.getBoundingClientRect() + if (!rect || canvasView.value.scale <= 0) { + return null + } + const canvasX = event.clientX - rect.left + const canvasY = event.clientY - rect.top + const imageX = (canvasX - canvasView.value.offsetX) / canvasView.value.scale + const imageY = (canvasY - canvasView.value.offsetY) / canvasView.value.scale + return { imageX, imageY } + } + + function isLocked(layer: Layer) { + return Boolean(layer.config?.locked) + } + + function createSnapshotMap(ids: string[]) { + // 拖拽/缩放期间基于起始快照反复计算,避免连续累加造成误差。 + return new Map( + layerList.value + .filter(entry => ids.includes(entry.id)) + .map(entry => [ + entry.id, + { + id: entry.id, + x: entry.x, + y: entry.y, + width: entry.width, + height: entry.height, + }, + ]), + ) + } + + function buildOtherRects(excludeIds: string[]): Rect[] { + const excludeSet = new Set(excludeIds) + return layerList.value + .filter(l => !excludeSet.has(l.id) && l.config?.visible !== false) + .map(l => ({ x: l.x, y: l.y, width: l.width, height: l.height })) + } + + function getSnapshotLayers(ids: string[], map: Map) { + return ids + .map((id) => { + const snapshot = map.get(id) + if (!snapshot) { + return null + } + return { + id: snapshot.id, + type: 'custom', + x: snapshot.x, + y: snapshot.y, + width: snapshot.width, + height: snapshot.height, + } as Layer + }) + .filter(Boolean) as Layer[] + } + + function startDrag(ids: string[], pointer: { imageX: number, imageY: number }, groupId?: string | null) { + setEditorEmitter('propertyPanel:suspend') + dragState.active = true + dragState.ids = ids + dragState.groupId = groupId ?? null + dragState.startPointerX = pointer.imageX + dragState.startPointerY = pointer.imageY + dragState.startRects = createSnapshotMap(ids) + dragState.startGroupBounds = dragState.groupId + ? (() => { + const group = getLayerGroupById(layerList.value, dragState.groupId || '') + if (!group) { + return null + } + return { + x: group.x, + y: group.y, + width: group.width, + height: group.height, + } + })() + : null + + window.addEventListener('pointermove', onLayerPointerMove) + window.addEventListener('pointerup', onLayerPointerUp) + } + + function onLayerPointerDown(payload: { event: PointerEvent, id: string }) { + const { event, id } = payload + if (event.button !== 0) { + return + } + if (!getCanvasElement() || canvasView.value.scale <= 0) { + return + } + + const pointer = getPointerImagePosition(event) + const layer = layerList.value.find(entry => entry.id === id) + if (!layer || !pointer || isLocked(layer)) { + return + } + + const additive = event.metaKey || event.ctrlKey || event.shiftKey + + // 拖动多选中的任一图层时,沿用当前可拖动的选区一起移动。 + const activeIds = (selectedLayerIds.value.includes(id) && selectedLayerIds.value.length > 1 + ? [...selectedLayerIds.value] + : [id] + ).filter((entryId) => { + const entry = layerList.value.find(layer => layer.id === entryId) + return entry ? !isLocked(entry) : false + }) + + if (!activeIds.length) { + return + } + + if (!additive && (!selectedLayerIds.value.includes(id) || selectedLayerIds.value.length <= 1)) { + layerStore.setSelectedLayerIds([id], id, { groupId: null }) + } + if (!additive) { + selectedLayerId.value = id + } + startDrag(activeIds, pointer) + event.preventDefault() + } + listenEditorEmitter('canvasStage:layerPointerDown', onLayerPointerDown) + + function onGroupPointerDown(payload: { event: PointerEvent, groupId: string }) { + const { event, groupId } = payload + if (event.button !== 0 || !getCanvasElement() || canvasView.value.scale <= 0) { + return + } + + const pointer = getPointerImagePosition(event) + const group = getLayerGroupById(layerList.value, groupId) + if (!group || !pointer || group.layers.some(isLocked)) { + return + } + + layerStore.setSelectedLayerIds(group.layerIds, group.layerIds[0] ?? '', { groupId }) + startDrag(group.layerIds, pointer, groupId) + event.preventDefault() + } + listenEditorEmitter('canvasStage:groupPointerDown', onGroupPointerDown) + + function onResizePointerDown(payload: { event: PointerEvent, id: string, handle: ResizeHandle }) { + const { event, id, handle } = payload + if (event.button !== 0) { + return + } + if (!canvasView.value.imageWidth || !canvasView.value.imageHeight) { + return + } + + const pointer = getPointerImagePosition(event) + const layer = layerList.value.find(entry => entry.id === id) + if (!layer || !pointer || isLocked(layer)) { + return + } + + event.preventDefault() + setEditorEmitter('propertyPanel:suspend') + layerStore.setSelectedLayerIds([id], id, { groupId: null }) + selectedLayerId.value = id + + resizeState.id = id + resizeState.groupId = null + resizeState.handle = handle + resizeState.startX = pointer.imageX + resizeState.startY = pointer.imageY + resizeState.startLeft = layer.x + resizeState.startTop = layer.y + resizeState.startRight = layer.x + layer.width + resizeState.startBottom = layer.y + layer.height + resizeState.startRects.clear() + + window.addEventListener('pointermove', onResizePointerMove) + window.addEventListener('pointerup', onResizePointerUp) + } + listenEditorEmitter('canvasStage:layerResizePointerDown', onResizePointerDown) + + function onGroupResizePointerDown(payload: { event: PointerEvent, groupId: string, handle: ResizeHandle }) { + const { event, groupId, handle } = payload + if (event.button !== 0 || !canvasView.value.imageWidth || !canvasView.value.imageHeight) { + return + } + + const pointer = getPointerImagePosition(event) + const group = getLayerGroupById(layerList.value, groupId) + if (!group || !pointer || group.layers.some(isLocked)) { + return + } + + event.preventDefault() + setEditorEmitter('propertyPanel:suspend') + layerStore.setSelectedLayerIds(group.layerIds, group.layerIds[0] ?? '', { groupId }) + + resizeState.id = null + resizeState.groupId = groupId + resizeState.handle = handle + resizeState.startX = pointer.imageX + resizeState.startY = pointer.imageY + resizeState.startLeft = group.x + resizeState.startTop = group.y + resizeState.startRight = group.x + group.width + resizeState.startBottom = group.y + group.height + resizeState.startRects = createSnapshotMap(group.layerIds) + + window.addEventListener('pointermove', onResizePointerMove) + window.addEventListener('pointerup', onResizePointerUp) + } + listenEditorEmitter('canvasStage:groupResizePointerDown', onGroupResizePointerDown) + + function onLayerPointerMove(event: PointerEvent) { + if (!dragState.active || !dragState.ids.length || !canvasView.value.imageWidth || !canvasView.value.imageHeight) { + return + } + const pointer = getPointerImagePosition(event) + if (!pointer) { + return + } + + const deltaX = pointer.imageX - dragState.startPointerX + const deltaY = pointer.imageY - dragState.startPointerY + const canvasSize = { width: canvasView.value.imageWidth, height: canvasView.value.imageHeight } + const skipSnap = event.altKey + + if (dragState.groupId && dragState.startGroupBounds) { + const bounds = dragState.startGroupBounds + const rawX = bounds.x + deltaX + const rawY = bounds.y + deltaY + let nextX: number + let nextY: number + + if (skipSnap) { + nextX = rawX + nextY = rawY + clearGuides() + } + else { + const targetRect: Rect = { x: rawX, y: rawY, width: bounds.width, height: bounds.height } + const snap = calcSnap(targetRect, buildOtherRects(dragState.ids), canvasSize) + nextX = snap.snappedX + nextY = snap.snappedY + updateGuides({ guides: snap.guides, distances: snap.distances }) + } + + nextX = clamp(nextX, 0, Math.max(0, canvasSize.width - bounds.width)) + nextY = clamp(nextY, 0, Math.max(0, canvasSize.height - bounds.height)) + + const updates = buildGroupPositionUpdates( + getSnapshotLayers(dragState.ids, dragState.startRects), + nextX - bounds.x, + nextY - bounds.y, + ) + setEditorEmitter('layer:updatePositions', updates) + return + } + + if (dragState.ids.length === 1) { + const id = dragState.ids[0] + const start = dragState.startRects.get(id) + if (!start) { + return + } + const rawX = start.x + deltaX + const rawY = start.y + deltaY + let nextX: number + let nextY: number + + if (skipSnap) { + nextX = rawX + nextY = rawY + clearGuides() + } + else { + const targetRect: Rect = { x: rawX, y: rawY, width: start.width, height: start.height } + const snap = calcSnap(targetRect, buildOtherRects([id]), canvasSize) + nextX = snap.snappedX + nextY = snap.snappedY + updateGuides({ guides: snap.guides, distances: snap.distances }) + } + + nextX = clamp(nextX, 0, Math.max(0, canvasSize.width - start.width)) + nextY = clamp(nextY, 0, Math.max(0, canvasSize.height - start.height)) + setEditorEmitter('layer:updatePosition', { id, nextX, nextY }) + return + } + + // 多选拖拽:用包围盒做吸附,delta 统一应用 + const allStarts = dragState.ids.map(id => dragState.startRects.get(id)).filter(Boolean) as LayerRectSnapshot[] + const bboxX = Math.min(...allStarts.map(s => s.x + deltaX)) + const bboxY = Math.min(...allStarts.map(s => s.y + deltaY)) + const bboxRight = Math.max(...allStarts.map(s => s.x + s.width + deltaX)) + const bboxBottom = Math.max(...allStarts.map(s => s.y + s.height + deltaY)) + const bboxRect: Rect = { x: bboxX, y: bboxY, width: bboxRight - bboxX, height: bboxBottom - bboxY } + + let snapDeltaX = 0 + let snapDeltaY = 0 + + if (skipSnap) { + clearGuides() + } + else { + const snap = calcSnap(bboxRect, buildOtherRects(dragState.ids), canvasSize) + snapDeltaX = snap.snappedX - bboxX + snapDeltaY = snap.snappedY - bboxY + updateGuides({ guides: snap.guides, distances: snap.distances }) + } + + const updates = dragState.ids + .map((id) => { + const start = dragState.startRects.get(id) + if (!start) { + return null + } + return { + id, + nextX: clamp(start.x + deltaX + snapDeltaX, 0, Math.max(0, canvasSize.width - start.width)), + nextY: clamp(start.y + deltaY + snapDeltaY, 0, Math.max(0, canvasSize.height - start.height)), + } + }) + .filter(Boolean) as Array<{ id: string, nextX: number, nextY: number }> + + setEditorEmitter('layer:updatePositions', updates) + } + + function onLayerPointerUp() { + dragState.active = false + dragState.ids = [] + dragState.groupId = null + dragState.startRects.clear() + dragState.startGroupBounds = null + clearGuides() + window.removeEventListener('pointermove', onLayerPointerMove) + window.removeEventListener('pointerup', onLayerPointerUp) + setEditorEmitter('propertyPanel:resume') + } + + function onResizePointerMove(event: PointerEvent) { + if (!resizeState.handle || !canvasView.value.imageWidth || !canvasView.value.imageHeight) { + return + } + + const pointer = getPointerImagePosition(event) + if (!pointer) { + return + } + + const deltaX = pointer.imageX - resizeState.startX + const deltaY = pointer.imageY - resizeState.startY + const minSize = 24 + const canvasSize = { width: canvasView.value.imageWidth, height: canvasView.value.imageHeight } + const skipSnap = event.altKey + + let left = resizeState.startLeft + let right = resizeState.startRight + let top = resizeState.startTop + let bottom = resizeState.startBottom + + // 先计算原始边缘位置 + if (resizeState.handle.includes('w')) { + left = resizeState.startLeft + deltaX + } + if (resizeState.handle.includes('e')) { + right = resizeState.startRight + deltaX + } + if (resizeState.handle.includes('n')) { + top = resizeState.startTop + deltaY + } + if (resizeState.handle.includes('s')) { + bottom = resizeState.startBottom + deltaY + } + + // 吸附 + if (!skipSnap) { + const edges: ('left' | 'right' | 'top' | 'bottom')[] = [] + if (resizeState.handle.includes('w')) + edges.push('left') + if (resizeState.handle.includes('e')) + edges.push('right') + if (resizeState.handle.includes('n')) + edges.push('top') + if (resizeState.handle.includes('s')) + edges.push('bottom') + + const currentRect: Rect = { x: left, y: top, width: right - left, height: bottom - top } + const excludeId = resizeState.id ? [resizeState.id] : [...resizeState.startRects.keys()] + const snap = calcResizeSnap(edges, currentRect, buildOtherRects(excludeId), canvasSize) + + if (snap.snappedEdges.left !== undefined) + left = snap.snappedEdges.left + if (snap.snappedEdges.right !== undefined) + right = snap.snappedEdges.right + if (snap.snappedEdges.top !== undefined) + top = snap.snappedEdges.top + if (snap.snappedEdges.bottom !== undefined) + bottom = snap.snappedEdges.bottom + + updateGuides({ guides: snap.guides, distances: snap.distances }) + } + else { + clearGuides() + } + + // clamp 边界约束 + if (resizeState.handle.includes('w')) + left = clamp(left, 0, resizeState.startRight - minSize) + if (resizeState.handle.includes('e')) + right = clamp(right, resizeState.startLeft + minSize, canvasSize.width) + if (resizeState.handle.includes('n')) + top = clamp(top, 0, resizeState.startBottom - minSize) + if (resizeState.handle.includes('s')) + bottom = clamp(bottom, resizeState.startTop + minSize, canvasSize.height) + + const nextWidth = clamp(right - left, minSize, canvasSize.width) + const nextHeight = clamp(bottom - top, minSize, canvasSize.height) + const nextX = clamp(left, 0, Math.max(0, canvasSize.width - nextWidth)) + const nextY = clamp(top, 0, Math.max(0, canvasSize.height - nextHeight)) + + if (resizeState.groupId) { + const updates = buildGroupRectUpdates( + getSnapshotLayers([...resizeState.startRects.keys()], resizeState.startRects), + { + x: resizeState.startLeft, + y: resizeState.startTop, + width: resizeState.startRight - resizeState.startLeft, + height: resizeState.startBottom - resizeState.startTop, + }, + { x: nextX, y: nextY, width: nextWidth, height: nextHeight }, + ) + setEditorEmitter('layer:updateRects', updates) + return + } + + if (!resizeState.id) { + return + } + + const layer = layerList.value.find(entry => entry.id === resizeState.id) + if (!layer || isLocked(layer)) { + return + } + + setEditorEmitter('layer:updateRect', { id: layer.id, nextX, nextY, nextWidth, nextHeight }) + } + + function onResizePointerUp() { + resizeState.id = null + resizeState.groupId = null + resizeState.handle = null + resizeState.startRects.clear() + clearGuides() + window.removeEventListener('pointermove', onResizePointerMove) + window.removeEventListener('pointerup', onResizePointerUp) + setEditorEmitter('propertyPanel:resume') + } + + onBeforeUnmount(() => { + window.removeEventListener('pointermove', onLayerPointerMove) + window.removeEventListener('pointerup', onLayerPointerUp) + window.removeEventListener('pointermove', onResizePointerMove) + window.removeEventListener('pointerup', onResizePointerUp) + }) + + return { + snapGuides, + onLayerPointerDown, + onResizePointerDown, + } +} diff --git a/packages/core/src/components/editor/canvas/modules/stage/composables/useSnapGuides.ts b/packages/core/src/components/editor/canvas/modules/stage/composables/useSnapGuides.ts new file mode 100644 index 0000000..b894c9c --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/stage/composables/useSnapGuides.ts @@ -0,0 +1,420 @@ +import { ref } from 'vue' + +// ---- 类型定义 ---- + +export interface Rect { + x: number + y: number + width: number + height: number +} + +export interface GuideLine { + type: 'edge' | 'center' | 'canvas' + direction: 'horizontal' | 'vertical' + position: number + start: number + end: number +} + +export interface DistanceLabel { + direction: 'horizontal' | 'vertical' + x: number + y: number + distance: number +} + +export interface SnapResult { + snappedX: number + snappedY: number + guides: GuideLine[] + distances: DistanceLabel[] +} + +export interface ResizeSnapResult { + snappedEdges: Partial> + guides: GuideLine[] + distances: DistanceLabel[] +} + +export interface SnapGuidesState { + guides: GuideLine[] + distances: DistanceLabel[] +} + +const SNAP_THRESHOLD = 8 + +// ---- 内部工具 ---- + +interface SnapCandidate { + targetLine: number + refLine: number + distance: number + type: 'edge' | 'center' | 'canvas' + targetEdge: string + refRect: Rect +} + +function extractVerticalLines(rect: Rect): { left: number, right: number, centerX: number } { + return { + left: rect.x, + right: rect.x + rect.width, + centerX: rect.x + rect.width / 2, + } +} + +function extractHorizontalLines(rect: Rect): { top: number, bottom: number, centerY: number } { + return { + top: rect.y, + bottom: rect.y + rect.height, + centerY: rect.y + rect.height / 2, + } +} + +function findBestSnap( + targetRect: Rect, + otherRects: Rect[], + canvasRect: Rect, + threshold: number, +) { + const allRefs: Array<{ rect: Rect, isCanvas: boolean }> = [ + ...otherRects.map(r => ({ rect: r, isCanvas: false })), + { rect: canvasRect, isCanvas: true }, + ] + + const vCandidates: SnapCandidate[] = [] + const hCandidates: SnapCandidate[] = [] + + const targetV = extractVerticalLines(targetRect) + const targetH = extractHorizontalLines(targetRect) + + for (const { rect: refRect, isCanvas } of allRefs) { + const refV = extractVerticalLines(refRect) + const refH = extractHorizontalLines(refRect) + const type = isCanvas ? 'canvas' as const : 'edge' as const + + for (const [tKey, tVal] of Object.entries(targetV)) { + for (const [rKey, rVal] of Object.entries(refV)) { + const dist = Math.abs(tVal - rVal) + if (dist <= threshold) { + const rType = (rKey === 'centerX' || tKey === 'centerX') && !isCanvas ? 'center' as const : type + vCandidates.push({ + targetLine: tVal, + refLine: rVal, + distance: dist, + type: rType, + targetEdge: tKey, + refRect, + }) + } + } + } + + for (const [tKey, tVal] of Object.entries(targetH)) { + for (const [rKey, rVal] of Object.entries(refH)) { + const dist = Math.abs(tVal - rVal) + if (dist <= threshold) { + const rType = (rKey === 'centerY' || tKey === 'centerY') && !isCanvas ? 'center' as const : type + hCandidates.push({ + targetLine: tVal, + refLine: rVal, + distance: dist, + type: rType, + targetEdge: tKey, + refRect, + }) + } + } + } + } + + const bestV = vCandidates.length > 0 + ? vCandidates.reduce((best, c) => c.distance < best.distance ? c : best) + : null + const bestH = hCandidates.length > 0 + ? hCandidates.reduce((best, c) => c.distance < best.distance ? c : best) + : null + + const matchedV = bestV ? vCandidates.filter(c => c.refLine === bestV.refLine) : [] + const matchedH = bestH ? hCandidates.filter(c => c.refLine === bestH.refLine) : [] + + return { bestV, bestH, matchedV, matchedH } +} + +function buildGuideLines( + matchedV: SnapCandidate[], + matchedH: SnapCandidate[], + snappedRect: Rect, +): GuideLine[] { + const guides: GuideLine[] = [] + + for (const m of matchedV) { + const start = Math.min(snappedRect.y, m.refRect.y) + const end = Math.max(snappedRect.y + snappedRect.height, m.refRect.y + m.refRect.height) + guides.push({ + type: m.type, + direction: 'vertical', + position: m.refLine, + start, + end, + }) + } + + for (const m of matchedH) { + const start = Math.min(snappedRect.x, m.refRect.x) + const end = Math.max(snappedRect.x + snappedRect.width, m.refRect.x + m.refRect.width) + guides.push({ + type: m.type, + direction: 'horizontal', + position: m.refLine, + start, + end, + }) + } + + // 去重(相同 direction + position 只保留范围最大的) + const deduped = new Map() + for (const g of guides) { + const key = `${g.direction}-${g.position}` + const existing = deduped.get(key) + if (!existing) { + deduped.set(key, g) + } + else { + existing.start = Math.min(existing.start, g.start) + existing.end = Math.max(existing.end, g.end) + } + } + + return [...deduped.values()] +} + +// ---- 等间距吸附 ---- + +function hasOverlap(a: Rect, b: Rect, axis: 'horizontal' | 'vertical'): boolean { + if (axis === 'horizontal') { + return a.y < b.y + b.height && a.y + a.height > b.y + } + return a.x < b.x + b.width && a.x + a.width > b.x +} + +function calcEqualSpacing( + targetRect: Rect, + otherRects: Rect[], + threshold: number, +): { deltaX: number, deltaY: number, distances: DistanceLabel[] } { + let deltaX = 0 + let deltaY = 0 + const distances: DistanceLabel[] = [] + + // 水平等间距 + const hNeighbors = otherRects.filter(r => hasOverlap(r, targetRect, 'horizontal')) + const leftNeighbor = hNeighbors + .filter(r => r.x + r.width <= targetRect.x) + .sort((a, b) => (b.x + b.width) - (a.x + a.width))[0] + const rightNeighbor = hNeighbors + .filter(r => r.x >= targetRect.x + targetRect.width) + .sort((a, b) => a.x - b.x)[0] + + if (leftNeighbor && rightNeighbor) { + const totalGap = rightNeighbor.x - (leftNeighbor.x + leftNeighbor.width) - targetRect.width + const equalX = leftNeighbor.x + leftNeighbor.width + totalGap / 2 + if (Math.abs(targetRect.x - equalX) <= threshold) { + deltaX = equalX - targetRect.x + const leftGap = Math.round(equalX - (leftNeighbor.x + leftNeighbor.width)) + const rightGap = Math.round(rightNeighbor.x - (equalX + targetRect.width)) + const midY = targetRect.y + targetRect.height / 2 + distances.push( + { direction: 'horizontal', x: leftNeighbor.x + leftNeighbor.width + leftGap / 2, y: midY, distance: leftGap }, + { direction: 'horizontal', x: equalX + targetRect.width + rightGap / 2, y: midY, distance: rightGap }, + ) + } + } + + // 垂直等间距 + const vNeighbors = otherRects.filter(r => hasOverlap(r, targetRect, 'vertical')) + const topNeighbor = vNeighbors + .filter(r => r.y + r.height <= targetRect.y) + .sort((a, b) => (b.y + b.height) - (a.y + a.height))[0] + const bottomNeighbor = vNeighbors + .filter(r => r.y >= targetRect.y + targetRect.height) + .sort((a, b) => a.y - b.y)[0] + + if (topNeighbor && bottomNeighbor) { + const totalGap = bottomNeighbor.y - (topNeighbor.y + topNeighbor.height) - targetRect.height + const equalY = topNeighbor.y + topNeighbor.height + totalGap / 2 + if (Math.abs(targetRect.y - equalY) <= threshold) { + deltaY = equalY - targetRect.y + const topGap = Math.round(equalY - (topNeighbor.y + topNeighbor.height)) + const bottomGap = Math.round(bottomNeighbor.y - (equalY + targetRect.height)) + const midX = targetRect.x + targetRect.width / 2 + distances.push( + { direction: 'vertical', x: midX, y: topNeighbor.y + topNeighbor.height + topGap / 2, distance: topGap }, + { direction: 'vertical', x: midX, y: equalY + targetRect.height + bottomGap / 2, distance: bottomGap }, + ) + } + } + + return { deltaX, deltaY, distances } +} + +// ---- 公开 API ---- + +function snapToGridFallback(value: number, gridSize = 10, threshold = 4) { + if (gridSize <= 0) { + return value + } + const snapped = Math.round(value / gridSize) * gridSize + return Math.abs(snapped - value) <= threshold ? snapped : value +} + +export function calcSnap( + targetRect: Rect, + otherRects: Rect[], + canvasSize: { width: number, height: number }, + threshold = SNAP_THRESHOLD, +): SnapResult { + const canvasRect: Rect = { x: 0, y: 0, width: canvasSize.width, height: canvasSize.height } + const { bestV, bestH, matchedV, matchedH } = findBestSnap(targetRect, otherRects, canvasRect, threshold) + + let snappedX = targetRect.x + let snappedY = targetRect.y + let guides: GuideLine[] = [] + let distances: DistanceLabel[] = [] + + const hasVSnap = bestV !== null + const hasHSnap = bestH !== null + + if (hasVSnap && bestV) { + snappedX = targetRect.x + (bestV.refLine - bestV.targetLine) + } + if (hasHSnap && bestH) { + snappedY = targetRect.y + (bestH.refLine - bestH.targetLine) + } + + const snappedRect: Rect = { x: snappedX, y: snappedY, width: targetRect.width, height: targetRect.height } + guides = buildGuideLines(matchedV, matchedH, snappedRect) + + // 等间距吸附:仅在对应方向无边缘/中线匹配时检测 + if (!hasVSnap || !hasHSnap) { + const eqRect: Rect = { x: snappedX, y: snappedY, width: targetRect.width, height: targetRect.height } + const eq = calcEqualSpacing(eqRect, otherRects, threshold) + if (!hasVSnap && eq.deltaX !== 0) { + snappedX += eq.deltaX + } + if (!hasHSnap && eq.deltaY !== 0) { + snappedY += eq.deltaY + } + distances = eq.distances + } + + // Fallback 到网格吸附 + // 水平等间距的 distances 标记为 'horizontal',影响 X 坐标;垂直同理 + if (!hasVSnap && distances.filter(d => d.direction === 'horizontal').length === 0) { + snappedX = snapToGridFallback(snappedX) + } + if (!hasHSnap && distances.filter(d => d.direction === 'vertical').length === 0) { + snappedY = snapToGridFallback(snappedY) + } + + return { snappedX, snappedY, guides, distances } +} + +export function calcResizeSnap( + edges: ('left' | 'right' | 'top' | 'bottom')[], + currentRect: Rect, + otherRects: Rect[], + canvasSize: { width: number, height: number }, + threshold = SNAP_THRESHOLD, +): ResizeSnapResult { + const canvasRect: Rect = { x: 0, y: 0, width: canvasSize.width, height: canvasSize.height } + const allRefs: Array<{ rect: Rect, isCanvas: boolean }> = [ + ...otherRects.map(r => ({ rect: r, isCanvas: false })), + { rect: canvasRect, isCanvas: true }, + ] + + const snappedEdges: Partial> = {} + const guides: GuideLine[] = [] + + for (const edge of edges) { + let edgePos: number + if (edge === 'left') { + edgePos = currentRect.x + } + else if (edge === 'right') { + edgePos = currentRect.x + currentRect.width + } + else if (edge === 'top') { + edgePos = currentRect.y + } + else { + edgePos = currentRect.y + currentRect.height + } + + const isVertical = edge === 'left' || edge === 'right' + let bestDist = threshold + 1 + let bestRef: number | null = null + let bestType: GuideLine['type'] = 'edge' + let bestRefRect: Rect | null = null + + for (const { rect: refRect, isCanvas } of allRefs) { + const refLines = isVertical + ? [refRect.x, refRect.x + refRect.width, refRect.x + refRect.width / 2] + : [refRect.y, refRect.y + refRect.height, refRect.y + refRect.height / 2] + + for (let i = 0; i < refLines.length; i++) { + const dist = Math.abs(edgePos - refLines[i]) + if (dist < bestDist) { + bestDist = dist + bestRef = refLines[i] + bestType = isCanvas ? 'canvas' : (i === 2 ? 'center' : 'edge') + bestRefRect = refRect + } + } + } + + if (bestRef !== null && bestRefRect) { + snappedEdges[edge] = bestRef + if (isVertical) { + guides.push({ + type: bestType, + direction: 'vertical', + position: bestRef, + start: Math.min(currentRect.y, bestRefRect.y), + end: Math.max(currentRect.y + currentRect.height, bestRefRect.y + bestRefRect.height), + }) + } + else { + guides.push({ + type: bestType, + direction: 'horizontal', + position: bestRef, + start: Math.min(currentRect.x, bestRefRect.x), + end: Math.max(currentRect.x + currentRect.width, bestRefRect.x + bestRefRect.width), + }) + } + } + } + + return { snappedEdges, guides, distances: [] } +} + +export function useSnapGuides() { + const snapGuides = ref({ guides: [], distances: [] }) + + function updateGuides(state: SnapGuidesState) { + snapGuides.value = state + } + + function clearGuides() { + snapGuides.value = { guides: [], distances: [] } + } + + return { + snapGuides, + updateGuides, + clearGuides, + calcSnap, + calcResizeSnap, + } +} diff --git a/packages/core/src/components/editor/canvas/modules/stage/index.vue b/packages/core/src/components/editor/canvas/modules/stage/index.vue new file mode 100644 index 0000000..a3353ec --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/stage/index.vue @@ -0,0 +1,1004 @@ + + + + + diff --git a/packages/core/src/components/editor/canvas/modules/template-library/index.vue b/packages/core/src/components/editor/canvas/modules/template-library/index.vue new file mode 100644 index 0000000..8e961a1 --- /dev/null +++ b/packages/core/src/components/editor/canvas/modules/template-library/index.vue @@ -0,0 +1,262 @@ + + + + + diff --git a/packages/core/src/components/editor/canvas/types.ts b/packages/core/src/components/editor/canvas/types.ts new file mode 100644 index 0000000..b8c8517 --- /dev/null +++ b/packages/core/src/components/editor/canvas/types.ts @@ -0,0 +1,18 @@ +import type { Ref } from 'vue' + +// 核心数据模型从 schema 重新导出,保持内部导入路径兼容 +export type { CanvasView, Layer, LayerBindingDefinition, LayerBindingValue, LayerGroup } from '@cslab-dcs/schema' +export type { ComponentEvent } from '@cslab-dcs/schema' + +// 编辑器专有类型 + +export interface CanvasStageExpose { + wrapperRef: Ref + canvasRef: Ref +} + +export interface PropertyPanelExpose { + panelRef: Ref +} + +export type ResizeHandle = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw' diff --git a/packages/core/src/components/editor/canvas/utils.ts b/packages/core/src/components/editor/canvas/utils.ts new file mode 100644 index 0000000..c2cc7f9 --- /dev/null +++ b/packages/core/src/components/editor/canvas/utils.ts @@ -0,0 +1,116 @@ +export function clamp(value: number, min: number, max: number) { + return Math.min(Math.max(value, min), max) +} + +export function snapToGrid(value: number, gridSize = 10, threshold = 4) { + if (gridSize <= 0) { + return value + } + // 只在接近网格线时吸附,避免强制对齐导致拖拽手感生硬。 + const snapped = Math.round(value / gridSize) * gridSize + return Math.abs(snapped - value) <= threshold ? snapped : value +} + +export interface RectLike { + x: number + y: number + width: number + height: number +} + +export function normalizeRect(startX: number, startY: number, endX: number, endY: number): RectLike { + const x = Math.min(startX, endX) + const y = Math.min(startY, endY) + return { + x, + y, + width: Math.abs(endX - startX), + height: Math.abs(endY - startY), + } +} + +export function rectIntersects(a: RectLike, b: RectLike) { + return a.x < b.x + b.width + && a.x + a.width > b.x + && a.y < b.y + b.height + && a.y + a.height > b.y +} + +export function cloneValue(value: T): T { + if (typeof structuredClone === 'function') { + try { + return structuredClone(value) + } + catch {} + } + return safeJsonClone(value) +} + +function safeJsonClone(value: T): T { + const seen = new WeakSet() + // 兜底克隆时主动过滤 DOM、循环引用和不可序列化值,保证编辑器数据可复制。 + const serialized = JSON.stringify(value, (_key, current) => { + if (typeof current === 'function' || typeof current === 'symbol') { + return undefined + } + if (typeof current === 'bigint') { + return Number(current) + } + if (typeof window !== 'undefined' && current === window) { + return undefined + } + if (typeof Node !== 'undefined' && current instanceof Node) { + return undefined + } + if (current && typeof current === 'object') { + if (seen.has(current)) { + return undefined + } + seen.add(current) + } + return current + }) + + if (serialized === undefined) { + return value + } + return JSON.parse(serialized) as T +} + +// 统一处理暴露的 DOM 引用(可能是 ref,也可能已是元素本身)。 +export function unwrapElement(target: unknown): T | null { + if (!target) { + return null + } + if (typeof target === 'object' && 'value' in (target as { value?: unknown })) { + return (target as { value?: T | null }).value ?? null + } + return target as T +} + +// 将图片缩放到 1x1 像素取样,作为“平均色”的近似值。 +export function sampleAverageColor(image: HTMLImageElement, fallback = 'rgb(245, 247, 250)') { + const sampleCanvas = document.createElement('canvas') + sampleCanvas.width = 1 + sampleCanvas.height = 1 + const ctx = sampleCanvas.getContext('2d') + if (!ctx) { + return fallback + } + ctx.drawImage(image, 0, 0, 1, 1) + const [red, green, blue, alpha] = ctx.getImageData(0, 0, 1, 1).data + if (alpha === 0) { + return fallback + } + return `rgb(${red}, ${green}, ${blue})` +} + +export function createLayerId() { + const layerIndex = Math.floor(Math.random() * 100) + 1 + return `layer-${Date.now()}-${layerIndex}` +} + +export function createGroupId() { + const randomSuffix = Math.random().toString(36).slice(2, 8) + return `group-${Date.now()}-${randomSuffix}` +} diff --git a/packages/core/src/components/editor/components/bar.vue b/packages/core/src/components/editor/components/bar.vue new file mode 100644 index 0000000..a07141b --- /dev/null +++ b/packages/core/src/components/editor/components/bar.vue @@ -0,0 +1,148 @@ + + + + + diff --git a/packages/core/src/components/editor/components/button.vue b/packages/core/src/components/editor/components/button.vue new file mode 100644 index 0000000..5f0f443 --- /dev/null +++ b/packages/core/src/components/editor/components/button.vue @@ -0,0 +1,123 @@ + + + + + diff --git a/packages/core/src/components/editor/components/canvas-switcher.vue b/packages/core/src/components/editor/components/canvas-switcher.vue new file mode 100644 index 0000000..9c738d8 --- /dev/null +++ b/packages/core/src/components/editor/components/canvas-switcher.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/packages/core/src/components/editor/components/expression-sandbox.ts b/packages/core/src/components/editor/components/expression-sandbox.ts new file mode 100644 index 0000000..2fb2e25 --- /dev/null +++ b/packages/core/src/components/editor/components/expression-sandbox.ts @@ -0,0 +1,24 @@ +/** + * 表达式安全沙盒 + * 基于 AST 白名单解释器,替代了不安全的 new Function() 实现 + */ + +import { evaluate, validate } from './expression' + +export function isExpressionSafe(expression: string): boolean { + return validate(expression).ok +} + +export function safeEvaluate(expression: string, scope: Record): unknown { + if (!isExpressionSafe(expression)) { + console.error('[expression-sandbox] 表达式校验失败,已拒绝执行:', expression) + return undefined + } + try { + return evaluate(expression, scope) + } + catch (error) { + console.error('[expression-sandbox] 表达式求值失败:', expression, error) + return undefined + } +} diff --git a/packages/core/src/components/editor/components/expression/evaluator.ts b/packages/core/src/components/editor/components/expression/evaluator.ts new file mode 100644 index 0000000..91f3aa8 --- /dev/null +++ b/packages/core/src/components/editor/components/expression/evaluator.ts @@ -0,0 +1,412 @@ +// AST 求值器 — 基于白名单的安全求值 + +import type { ASTNode } from './types' +import { parse } from './parser' + +// 最大递归深度 +const MAX_DEPTH = 50 + +// 禁止访问的属性名 +const FORBIDDEN_PROPERTIES = new Set([ + 'constructor', + '__proto__', + 'prototype', + '__defineGetter__', + '__defineSetter__', + '__lookupGetter__', + '__lookupSetter__', +]) + +// 禁止作为标识符的名称 +const FORBIDDEN_IDENTIFIERS = new Set([ + 'this', + 'globalThis', + 'window', + 'self', + 'global', + 'document', + 'eval', + 'Function', + 'import', + 'require', + 'process', + 'Proxy', + 'Reflect', + 'WeakRef', + 'FinalizationRegistry', + 'setTimeout', + 'setInterval', + 'fetch', + 'XMLHttpRequest', + 'WebSocket', + 'Worker', +]) + +// 白名单:允许在 scope 中提供的全局对象及其方法 +const ALLOWED_METHODS: Record> = { + Math: new Set(['abs', 'ceil', 'floor', 'round', 'min', 'max', 'pow', 'sqrt', 'log', 'sign', 'trunc', 'random']), + Number: new Set(['isFinite', 'isInteger', 'isNaN', 'parseFloat', 'parseInt']), + String: new Set(['fromCharCode']), + Date: new Set(['now']), +} + +// Boolean 作为转换函数,允许直接调用 +const ALLOWED_CALLABLE = new Set(['Boolean']) + +// 字符串实例方法白名单 +const STRING_METHODS = new Set([ + 'trim', + 'toUpperCase', + 'toLowerCase', + 'indexOf', + 'includes', + 'startsWith', + 'endsWith', + 'slice', + 'substring', + 'split', + 'replace', + 'charAt', +]) + +// 数组实例方法白名单 +const ARRAY_METHODS = new Set([ + 'indexOf', + 'includes', + 'join', + 'slice', + 'map', + 'filter', + 'find', + 'some', + 'every', +]) + +// 数字实例方法白名单 +const NUMBER_METHODS = new Set(['toFixed', 'toString']) + +// 字符串/数组只读属性白名单 +const ALLOWED_READONLY_PROPS = new Set(['length']) + +// 验证 String.fromCharCode 参数是否在可打印字符范围内 +function isAllowedCharCode(code: unknown): boolean { + return typeof code === 'number' && code >= 32 && code <= 126 +} + +// 检查属性访问是否安全 +function assertPropertySafe(prop: string): void { + if (FORBIDDEN_PROPERTIES.has(prop)) { + throw new Error(`禁止访问属性 '${prop}'`) + } +} + +// 解析成员表达式的 object 部分,判断是否是白名单对象 +function resolveCallTarget( + node: ASTNode, + scope: Record, + depth: number, +): { target: unknown, methodName: string } | null { + if (node.type !== 'MemberExpression') + return null + + const methodNode = node.computed ? null : node.property + if (!methodNode || methodNode.type !== 'Identifier') + return null + + const methodName = methodNode.name + assertPropertySafe(methodName) + + const target = evaluateNode(node.object, scope, depth + 1) + return { target, methodName } +} + +// 核心求值函数 +function evaluateNode(node: ASTNode, scope: Record, depth: number): unknown { + if (depth > MAX_DEPTH) { + throw new Error('递归深度超过限制') + } + + switch (node.type) { + case 'NumericLiteral': + return node.value + + case 'StringLiteral': + return node.value + + case 'BooleanLiteral': + return node.value + + case 'NullLiteral': + return null + + case 'UndefinedLiteral': + return undefined + + case 'Identifier': { + if (FORBIDDEN_IDENTIFIERS.has(node.name)) { + throw new Error(`禁止访问标识符 '${node.name}'`) + } + if (!(node.name in scope)) { + throw new Error(`未定义的标识符 '${node.name}'`) + } + return scope[node.name] + } + + case 'ArrayExpression': + return node.elements.map(el => evaluateNode(el, scope, depth + 1)) + + case 'BinaryExpression': { + const left = evaluateNode(node.left, scope, depth + 1) + const right = evaluateNode(node.right, scope, depth + 1) + return evaluateBinaryOp(node.operator, left, right) + } + + case 'LogicalExpression': { + const leftVal = evaluateNode(node.left, scope, depth + 1) + if (node.operator === '&&') { + return leftVal ? evaluateNode(node.right, scope, depth + 1) : leftVal + } + // || + return leftVal || evaluateNode(node.right, scope, depth + 1) + } + + case 'UnaryExpression': { + const operand = evaluateNode(node.operand, scope, depth + 1) + switch (node.operator) { + case '!': return !operand + case '-': return -(operand as number) + case '+': return +(operand as number) + } + break + } + + case 'TypeofExpression': { + // typeof 对未定义标识符不抛错 + try { + const val = evaluateNode(node.operand, scope, depth + 1) + return typeof val + } + catch { + return 'undefined' + } + } + + case 'ConditionalExpression': { + const test = evaluateNode(node.test, scope, depth + 1) + return test + ? evaluateNode(node.consequent, scope, depth + 1) + : evaluateNode(node.alternate, scope, depth + 1) + } + + case 'NullishCoalescing': { + const leftResult = evaluateNode(node.left, scope, depth + 1) + return leftResult ?? evaluateNode(node.right, scope, depth + 1) + } + + case 'MemberExpression': { + const obj = evaluateNode(node.object, scope, depth + 1) + const prop = resolveMemberProperty(node, scope, depth) + const propStr = String(prop) + + assertPropertySafe(propStr) + + // null/undefined 不可访问 + if (obj === null || obj === undefined) { + throw new Error(`无法访问 ${obj} 的属性 '${propStr}'`) + } + + // 对于白名单对象(Math 等),允许访问其白名单方法 + if (typeof obj === 'object' || typeof obj === 'function') { + const objName = findScopeKeyForValue(scope, obj) + if (objName && ALLOWED_METHODS[objName]) { + if (ALLOWED_METHODS[objName].has(propStr)) { + return (obj as Record)[propStr] + } + throw new Error(`不允许访问 ${objName}.${propStr}`) + } + } + + // 字符串属性/方法访问 + if (typeof obj === 'string') { + if (ALLOWED_READONLY_PROPS.has(propStr)) + return (obj as unknown as Record)[propStr] + if (STRING_METHODS.has(propStr)) + return (obj as unknown as Record unknown>)[propStr].bind(obj) + throw new Error(`不允许访问字符串的 '${propStr}' 属性`) + } + + // 数组属性/方法访问 + if (Array.isArray(obj)) { + if (ALLOWED_READONLY_PROPS.has(propStr)) + return (obj as unknown as Record)[propStr] + if (ARRAY_METHODS.has(propStr)) + return (obj as unknown as Record unknown>)[propStr].bind(obj) + // 允许按数字索引访问数组元素 + const idx = Number(propStr) + if (Number.isInteger(idx) && idx >= 0 && idx < obj.length) { + return obj[idx] + } + throw new Error(`不允许访问数组的 '${propStr}' 属性`) + } + + // 数字属性/方法访问 + if (typeof obj === 'number') { + if (NUMBER_METHODS.has(propStr)) + return (obj as unknown as Record unknown>)[propStr].bind(obj) + throw new Error(`不允许访问数字的 '${propStr}' 属性`) + } + + // 普通对象属性访问(scope 中的用户数据对象) + if (typeof obj === 'object' && obj !== null) { + if (Object.hasOwn(obj, propStr)) { + return (obj as Record)[propStr] + } + // 允许返回 undefined 而不是抛错,和 JS 行为一致 + return undefined + } + + throw new Error(`不允许在 ${typeof obj} 类型上访问属性 '${propStr}'`) + } + + case 'CallExpression': { + return evaluateCall(node, scope, depth) + } + } + + throw new Error(`未知的节点类型: ${(node as ASTNode).type}`) +} + +// 解析成员表达式的属性 +function resolveMemberProperty(node: ASTNode & { type: 'MemberExpression' }, scope: Record, depth: number): unknown { + if (node.computed) { + return evaluateNode(node.property, scope, depth + 1) + } + if (node.property.type === 'Identifier') { + return node.property.name + } + throw new Error('无效的成员表达式属性') +} + +// 在 scope 中查找某个值对应的 key +function findScopeKeyForValue(scope: Record, value: unknown): string | undefined { + for (const key of Object.keys(scope)) { + if (scope[key] === value) + return key + } + return undefined +} + +// 函数调用求值 +function evaluateCall(node: ASTNode & { type: 'CallExpression' }, scope: Record, depth: number): unknown { + const args = node.arguments.map(arg => evaluateNode(arg, scope, depth + 1)) + + // 如果 callee 是成员表达式(如 Math.round(...)) + if (node.callee.type === 'MemberExpression') { + const resolved = resolveCallTarget(node.callee, scope, depth) + if (!resolved) { + throw new Error('无效的函数调用') + } + + const { target, methodName } = resolved + + // 白名单对象方法调用 + if (target !== null && target !== undefined) { + const objName = findScopeKeyForValue(scope, target) + + // String.fromCharCode 特殊处理 + if (objName === 'String' && methodName === 'fromCharCode') { + if (!args.every(isAllowedCharCode)) { + throw new Error('String.fromCharCode 参数超出允许的可打印字符范围') + } + return String.fromCharCode(...(args as number[])) + } + + // 白名单静态方法 + if (objName && ALLOWED_METHODS[objName]?.has(methodName)) { + const method = (target as Record unknown>)[methodName] + if (typeof method === 'function') { + return method.apply(target, args) + } + } + + // 字符串实例方法 + if (typeof target === 'string' && STRING_METHODS.has(methodName)) { + const method = (target as unknown as Record unknown>)[methodName] + return method.apply(target, args) + } + + // 数组实例方法 + if (Array.isArray(target) && ARRAY_METHODS.has(methodName)) { + // 数组方法的回调参数安全性:确保回调是 scope 中的函数或简单的箭头函数 AST + const method = (target as unknown as Record unknown>)[methodName] + return method.apply(target, args) + } + + // 数字实例方法 + if (typeof target === 'number' && NUMBER_METHODS.has(methodName)) { + const method = (target as unknown as Record unknown>)[methodName] + return method.apply(target, args) + } + } + + throw new Error(`不允许调用 ${methodName}`) + } + + // 直接函数调用(如 Boolean(...)) + if (node.callee.type === 'Identifier') { + const fnName = node.callee.name + + if (FORBIDDEN_IDENTIFIERS.has(fnName)) { + throw new Error(`禁止调用 '${fnName}'`) + } + + if (ALLOWED_CALLABLE.has(fnName) && fnName in scope) { + const fn = scope[fnName] + if (typeof fn === 'function') { + return fn(...args) + } + } + + // 检查是否是 scope 中用户提供的函数 + if (fnName in scope) { + const fn = scope[fnName] + if (typeof fn === 'function') { + // 只允许白名单函数 + const objName = findScopeKeyForValue(scope, fn) + if (objName && ALLOWED_CALLABLE.has(objName)) { + return fn(...args) + } + } + } + + throw new Error(`不允许调用函数 '${fnName}'`) + } + + throw new Error('不允许的函数调用形式') +} + +// 二元运算符求值 +function evaluateBinaryOp(op: string, left: unknown, right: unknown): unknown { + switch (op) { + case '+': return (left as number) + (right as number) + case '-': return (left as number) - (right as number) + case '*': return (left as number) * (right as number) + case '/': return (left as number) / (right as number) + case '%': return (left as number) % (right as number) + case '===': return left === right + case '!==': return left !== right + case '==': return left == right // eslint-disable-line eqeqeq + case '!=': return left != right // eslint-disable-line eqeqeq + case '>': return (left as number) > (right as number) + case '<': return (left as number) < (right as number) + case '>=': return (left as number) >= (right as number) + case '<=': return (left as number) <= (right as number) + default: throw new Error(`未知的运算符 '${op}'`) + } +} + +// 公开 API:求值 +export function evaluate(expression: string, scope: Record): unknown { + const ast = parse(expression) + return evaluateNode(ast, scope, 0) +} diff --git a/packages/core/src/components/editor/components/expression/index.ts b/packages/core/src/components/editor/components/expression/index.ts new file mode 100644 index 0000000..589c2c9 --- /dev/null +++ b/packages/core/src/components/editor/components/expression/index.ts @@ -0,0 +1,39 @@ +// 表达式模块公开 API + +import type { ValidateResult } from './types' +import { evaluate } from './evaluator' +import { parse } from './parser' + +export { evaluate } from './evaluator' +export { parse } from './parser' +export { tokenize } from './tokenizer' +export type { ASTNode, ValidateResult } from './types' + +// 校验表达式是否合法(仅检查语法,不求值) +export function validate(expression: string): ValidateResult { + if (!expression || typeof expression !== 'string') { + return { ok: false, error: '表达式不能为空' } + } + + const trimmed = expression.trim() + if (!trimmed) { + return { ok: false, error: '表达式不能为空' } + } + + try { + parse(trimmed) + return { ok: true } + } + catch (e) { + return { ok: false, error: (e as Error).message } + } +} + +// 安全求值(校验 + 求值) +export function safeEval(expression: string, scope: Record): unknown { + const result = validate(expression) + if (!result.ok) { + throw new Error(result.error) + } + return evaluate(expression, scope) +} diff --git a/packages/core/src/components/editor/components/expression/parser.ts b/packages/core/src/components/editor/components/expression/parser.ts new file mode 100644 index 0000000..3422798 --- /dev/null +++ b/packages/core/src/components/editor/components/expression/parser.ts @@ -0,0 +1,350 @@ +// 递归下降解析器 + +import type { ASTNode, BinaryOperator, Token } from './types' +import { tokenize } from './tokenizer' + +// 最大节点数限制,防止 DoS +const MAX_NODE_COUNT = 1000 + +class Parser { + private tokens: Token[] + private pos = 0 + private nodeCount = 0 + + constructor(tokens: Token[]) { + this.tokens = tokens + } + + // 获取当前 token + private current(): Token { + return this.tokens[this.pos] + } + + // 消费当前 token 并移动到下一个 + private advance(): Token { + const token = this.tokens[this.pos] + this.pos++ + return token + } + + // 期望当前 token 匹配,匹配则消费,否则报错 + private expect(type: Token['type'], value?: string): Token { + const token = this.current() + if (token.type !== type || (value !== undefined && token.value !== value)) { + throw new SyntaxError( + `期望 ${type}${value ? ` '${value}'` : ''},得到 ${token.type} '${token.value}',位置 ${token.position}`, + ) + } + return this.advance() + } + + // 记录新节点 + private trackNode(): void { + this.nodeCount++ + if (this.nodeCount > MAX_NODE_COUNT) { + throw new Error('表达式过于复杂,超过节点数限制') + } + } + + // 入口:解析整个表达式 + parse(): ASTNode { + const node = this.parseTernary() + if (this.current().type !== 'EOF') { + throw new SyntaxError(`意外的 token '${this.current().value}',位置 ${this.current().position}`) + } + return node + } + + // 三元表达式:test ? consequent : alternate + private parseTernary(): ASTNode { + let node = this.parseNullishCoalescing() + + if (this.current().type === 'Punctuation' && this.current().value === '?') { + this.advance() + this.trackNode() + const consequent = this.parseTernary() + this.expect('Punctuation', ':') + const alternate = this.parseTernary() + node = { type: 'ConditionalExpression', test: node, consequent, alternate } + } + + return node + } + + // 空值合并:a ?? b + private parseNullishCoalescing(): ASTNode { + let node = this.parseLogicalOr() + + while (this.current().type === 'Operator' && this.current().value === '??') { + this.advance() + this.trackNode() + const right = this.parseLogicalOr() + node = { type: 'NullishCoalescing', left: node, right } + } + + return node + } + + // 逻辑或:a || b + private parseLogicalOr(): ASTNode { + let node = this.parseLogicalAnd() + + while (this.current().type === 'Operator' && this.current().value === '||') { + this.advance() + this.trackNode() + const right = this.parseLogicalAnd() + node = { type: 'LogicalExpression', operator: '||', left: node, right } + } + + return node + } + + // 逻辑与:a && b + private parseLogicalAnd(): ASTNode { + let node = this.parseEquality() + + while (this.current().type === 'Operator' && this.current().value === '&&') { + this.advance() + this.trackNode() + const right = this.parseEquality() + node = { type: 'LogicalExpression', operator: '&&', left: node, right } + } + + return node + } + + // 相等比较:===, !==, ==, != + private parseEquality(): ASTNode { + let node = this.parseComparison() + + while ( + this.current().type === 'Operator' + && (this.current().value === '===' + || this.current().value === '!==' + || this.current().value === '==' + || this.current().value === '!=') + ) { + const op = this.advance().value as BinaryOperator + this.trackNode() + const right = this.parseComparison() + node = { type: 'BinaryExpression', operator: op, left: node, right } + } + + return node + } + + // 大小比较:>, <, >=, <= + private parseComparison(): ASTNode { + let node = this.parseAdditive() + + while ( + this.current().type === 'Operator' + && (this.current().value === '>' + || this.current().value === '<' + || this.current().value === '>=' + || this.current().value === '<=') + ) { + const op = this.advance().value as BinaryOperator + this.trackNode() + const right = this.parseAdditive() + node = { type: 'BinaryExpression', operator: op, left: node, right } + } + + return node + } + + // 加减:+, - + private parseAdditive(): ASTNode { + let node = this.parseMultiplicative() + + while ( + this.current().type === 'Operator' + && (this.current().value === '+' || this.current().value === '-') + ) { + const op = this.advance().value as BinaryOperator + this.trackNode() + const right = this.parseMultiplicative() + node = { type: 'BinaryExpression', operator: op, left: node, right } + } + + return node + } + + // 乘除模:*, /, % + private parseMultiplicative(): ASTNode { + let node = this.parseUnary() + + while ( + this.current().type === 'Operator' + && (this.current().value === '*' + || this.current().value === '/' + || this.current().value === '%') + ) { + const op = this.advance().value as BinaryOperator + this.trackNode() + const right = this.parseUnary() + node = { type: 'BinaryExpression', operator: op, left: node, right } + } + + return node + } + + // 一元运算:!, -, +, typeof + private parseUnary(): ASTNode { + const token = this.current() + + // typeof 运算符 + if (token.type === 'Operator' && token.value === 'typeof') { + this.advance() + this.trackNode() + const operand = this.parseUnary() + return { type: 'TypeofExpression', operand } + } + + // !, -, + 一元运算 + if ( + token.type === 'Operator' + && (token.value === '!' || token.value === '-' || token.value === '+') + ) { + const op = this.advance().value as '!' | '-' | '+' + this.trackNode() + const operand = this.parseUnary() + return { type: 'UnaryExpression', operator: op, operand } + } + + return this.parseCallAndMember() + } + + // 成员访问和函数调用:obj.prop, obj['key'], fn(args) + private parseCallAndMember(): ASTNode { + let node = this.parsePrimary() + + while (true) { + const token = this.current() + + // 属性访问 obj.prop + if (token.type === 'Punctuation' && token.value === '.') { + this.advance() + this.trackNode() + const propToken = this.expect('Identifier') + node = { + type: 'MemberExpression', + object: node, + property: { type: 'Identifier', name: propToken.value }, + computed: false, + } + continue + } + + // 计算属性访问 obj[expr] + if (token.type === 'Punctuation' && token.value === '[') { + this.advance() + this.trackNode() + const property = this.parseTernary() + this.expect('Punctuation', ']') + node = { + type: 'MemberExpression', + object: node, + property, + computed: true, + } + continue + } + + // 函数调用 fn(args) + if (token.type === 'Punctuation' && token.value === '(') { + this.advance() + this.trackNode() + const args: ASTNode[] = [] + if (!(this.current().type === 'Punctuation' && this.current().value === ')')) { + args.push(this.parseTernary()) + while (this.current().type === 'Punctuation' && this.current().value === ',') { + this.advance() + args.push(this.parseTernary()) + } + } + this.expect('Punctuation', ')') + node = { type: 'CallExpression', callee: node, arguments: args } + continue + } + + break + } + + return node + } + + // 基本值 + private parsePrimary(): ASTNode { + const token = this.current() + this.trackNode() + + // 数字 + if (token.type === 'Number') { + this.advance() + return { type: 'NumericLiteral', value: Number(token.value) } + } + + // 字符串 + if (token.type === 'String') { + this.advance() + return { type: 'StringLiteral', value: token.value } + } + + // 布尔 + if (token.type === 'Boolean') { + this.advance() + return { type: 'BooleanLiteral', value: token.value === 'true' } + } + + // null + if (token.type === 'Null') { + this.advance() + return { type: 'NullLiteral' } + } + + // undefined + if (token.type === 'Undefined') { + this.advance() + return { type: 'UndefinedLiteral' } + } + + // 标识符 + if (token.type === 'Identifier') { + this.advance() + return { type: 'Identifier', name: token.value } + } + + // 括号分组 (expr) + if (token.type === 'Punctuation' && token.value === '(') { + this.advance() + const expr = this.parseTernary() + this.expect('Punctuation', ')') + return expr + } + + // 数组字面量 [a, b, c] + if (token.type === 'Punctuation' && token.value === '[') { + this.advance() + const elements: ASTNode[] = [] + if (!(this.current().type === 'Punctuation' && this.current().value === ']')) { + elements.push(this.parseTernary()) + while (this.current().type === 'Punctuation' && this.current().value === ',') { + this.advance() + elements.push(this.parseTernary()) + } + } + this.expect('Punctuation', ']') + return { type: 'ArrayExpression', elements } + } + + throw new SyntaxError(`意外的 token '${token.value}',位置 ${token.position}`) + } +} + +// 解析表达式字符串为 AST +export function parse(input: string): ASTNode { + const tokens = tokenize(input) + const parser = new Parser(tokens) + return parser.parse() +} diff --git a/packages/core/src/components/editor/components/expression/tokenizer.ts b/packages/core/src/components/editor/components/expression/tokenizer.ts new file mode 100644 index 0000000..c002de7 --- /dev/null +++ b/packages/core/src/components/editor/components/expression/tokenizer.ts @@ -0,0 +1,154 @@ +// 词法分析器 + +import type { Token } from './types' + +// 多字符运算符(按长度降序排列,优先匹配更长的) +const MULTI_CHAR_OPERATORS = ['===', '!==', '>=', '<=', '==', '!=', '&&', '||', '??'] + +// 单字符运算符 +const SINGLE_CHAR_OPERATORS = new Set(['+', '-', '*', '/', '%', '>', '<', '!']) + +// 标点符号 +const PUNCTUATIONS = new Set(['(', ')', '[', ']', '.', ',', ':', '?']) + +// 关键字映射(使用 Map 避免 Object.prototype 污染) +const KEYWORDS = new Map([ + ['true', 'Boolean'], + ['false', 'Boolean'], + ['null', 'Null'], + ['undefined', 'Undefined'], + ['typeof', 'Operator'], +]) + +// 模块级正则表达式,避免循环内重复编译 +const RE_WHITESPACE = /\s/ +const RE_DIGIT = /\d/ +const RE_IDENT_START = /[a-z_$]/i +const RE_IDENT_CHAR = /[\w$]/ + +export function tokenize(input: string): Token[] { + const tokens: Token[] = [] + let pos = 0 + + while (pos < input.length) { + // 跳过空白 + if (RE_WHITESPACE.test(input[pos])) { + pos++ + continue + } + + const ch = input[pos] + + // 数字字面量(整数和小数) + if (RE_DIGIT.test(ch) || (ch === '.' && pos + 1 < input.length && RE_DIGIT.test(input[pos + 1]))) { + const start = pos + // 整数部分 + while (pos < input.length && RE_DIGIT.test(input[pos])) + pos++ + // 小数部分 + if (pos < input.length && input[pos] === '.' && pos + 1 < input.length && RE_DIGIT.test(input[pos + 1])) { + pos++ // 跳过 '.' + while (pos < input.length && RE_DIGIT.test(input[pos])) + pos++ + } + tokens.push({ type: 'Number', value: input.slice(start, pos), position: start }) + continue + } + + // 字符串字面量(单引号或双引号) + if (ch === '\'' || ch === '"') { + const quote = ch + const start = pos + pos++ // 跳过开始引号 + let str = '' + while (pos < input.length && input[pos] !== quote) { + if (input[pos] === '\\' && pos + 1 < input.length) { + // 处理转义字符 + pos++ + const escaped = input[pos] + switch (escaped) { + case 'n': + str += '\n' + break + case 't': + str += '\t' + break + case 'r': + str += '\r' + break + case '\\': + str += '\\' + break + case '\'': + str += '\'' + break + case '"': + str += '"' + break + default: + str += escaped + } + pos++ + } + else { + str += input[pos] + pos++ + } + } + if (pos >= input.length) { + throw new SyntaxError(`未闭合的字符串字面量,位置 ${start}`) + } + pos++ // 跳过结束引号 + tokens.push({ type: 'String', value: str, position: start }) + continue + } + + // 多字符运算符 + let matched = false + for (const op of MULTI_CHAR_OPERATORS) { + if (input.startsWith(op, pos)) { + tokens.push({ type: 'Operator', value: op, position: pos }) + pos += op.length + matched = true + break + } + } + if (matched) + continue + + // 单字符运算符 + if (SINGLE_CHAR_OPERATORS.has(ch)) { + tokens.push({ type: 'Operator', value: ch, position: pos }) + pos++ + continue + } + + // 标点符号 + if (PUNCTUATIONS.has(ch)) { + tokens.push({ type: 'Punctuation', value: ch, position: pos }) + pos++ + continue + } + + // 标识符或关键字 + if (RE_IDENT_START.test(ch)) { + const start = pos + while (pos < input.length && RE_IDENT_CHAR.test(input[pos])) + pos++ + const word = input.slice(start, pos) + const keywordType = KEYWORDS.get(word) + if (keywordType) { + tokens.push({ type: keywordType, value: word, position: start }) + } + else { + tokens.push({ type: 'Identifier', value: word, position: start }) + } + continue + } + + throw new SyntaxError(`无法识别的字符 '${ch}',位置 ${pos}`) + } + + tokens.push({ type: 'EOF', value: '', position: pos }) + return tokens +} diff --git a/packages/core/src/components/editor/components/expression/types.ts b/packages/core/src/components/editor/components/expression/types.ts new file mode 100644 index 0000000..84fbcdc --- /dev/null +++ b/packages/core/src/components/editor/components/expression/types.ts @@ -0,0 +1,135 @@ +// AST 节点类型定义 + +export type ASTNode + = | NumericLiteral + | StringLiteral + | BooleanLiteral + | NullLiteral + | UndefinedLiteral + | Identifier + | BinaryExpression + | LogicalExpression + | UnaryExpression + | ConditionalExpression + | MemberExpression + | CallExpression + | ArrayExpression + | NullishCoalescing + | TypeofExpression + +export interface NumericLiteral { + type: 'NumericLiteral' + value: number +} + +export interface StringLiteral { + type: 'StringLiteral' + value: string +} + +export interface BooleanLiteral { + type: 'BooleanLiteral' + value: boolean +} + +export interface NullLiteral { + type: 'NullLiteral' +} + +export interface UndefinedLiteral { + type: 'UndefinedLiteral' +} + +export interface Identifier { + type: 'Identifier' + name: string +} + +export type BinaryOperator + = | '+' | '-' | '*' | '/' | '%' + | '===' | '!==' | '==' | '!=' + | '>' | '<' | '>=' | '<=' + +export interface BinaryExpression { + type: 'BinaryExpression' + operator: BinaryOperator + left: ASTNode + right: ASTNode +} + +export type LogicalOperator = '&&' | '||' + +export interface LogicalExpression { + type: 'LogicalExpression' + operator: LogicalOperator + left: ASTNode + right: ASTNode +} + +export type UnaryOperator = '!' | '-' | '+' + +export interface UnaryExpression { + type: 'UnaryExpression' + operator: UnaryOperator + operand: ASTNode +} + +export interface TypeofExpression { + type: 'TypeofExpression' + operand: ASTNode +} + +export interface ConditionalExpression { + type: 'ConditionalExpression' + test: ASTNode + consequent: ASTNode + alternate: ASTNode +} + +export interface NullishCoalescing { + type: 'NullishCoalescing' + left: ASTNode + right: ASTNode +} + +export interface MemberExpression { + type: 'MemberExpression' + object: ASTNode + property: ASTNode + computed: boolean // true = obj[expr], false = obj.prop +} + +export interface CallExpression { + type: 'CallExpression' + callee: ASTNode + arguments: ASTNode[] +} + +export interface ArrayExpression { + type: 'ArrayExpression' + elements: ASTNode[] +} + +// Token 类型定义 +export type TokenType + = | 'Number' + | 'String' + | 'Boolean' + | 'Null' + | 'Undefined' + | 'Identifier' + | 'Operator' + | 'Punctuation' + | 'EOF' + +export interface Token { + type: TokenType + value: string + position: number +} + +// 校验结果 +export interface ValidateResult { + ok: boolean + error?: string +} diff --git a/packages/core/src/components/editor/components/index.vue b/packages/core/src/components/editor/components/index.vue new file mode 100644 index 0000000..b2539fd --- /dev/null +++ b/packages/core/src/components/editor/components/index.vue @@ -0,0 +1,200 @@ + + + + + diff --git a/packages/core/src/components/editor/components/number.vue b/packages/core/src/components/editor/components/number.vue new file mode 100644 index 0000000..d28a55b --- /dev/null +++ b/packages/core/src/components/editor/components/number.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/packages/core/src/components/editor/components/pid-controller.vue b/packages/core/src/components/editor/components/pid-controller.vue new file mode 100644 index 0000000..c8d7b35 --- /dev/null +++ b/packages/core/src/components/editor/components/pid-controller.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/packages/core/src/components/editor/components/rect.vue b/packages/core/src/components/editor/components/rect.vue new file mode 100644 index 0000000..7936399 --- /dev/null +++ b/packages/core/src/components/editor/components/rect.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/packages/core/src/components/editor/components/runtime.ts b/packages/core/src/components/editor/components/runtime.ts new file mode 100644 index 0000000..cc0946d --- /dev/null +++ b/packages/core/src/components/editor/components/runtime.ts @@ -0,0 +1,430 @@ +import type { Layer, LayerBindingDefinition, LayerBindingValue } from '@cslab-dcs/schema' +import type { CanvasRuntimeVariable } from '../canvas/context/runtime' +import { get } from 'es-toolkit/compat' +import { safeEvaluate } from './expression-sandbox' + +const DEFAULT_RECT_FILL = '#D7EBFF' +const HEX_COLOR_RE = /^#(?:[\dA-F]{3}|[\dA-F]{6})$/i +const DEFAULT_BORDER_STYLE = 'solid' + +function toFiniteNumber(value: unknown, fallback: number) { + if (typeof value === 'number' && Number.isFinite(value)) { + return value + } + const parsed = Number(value) + return Number.isFinite(parsed) ? parsed : fallback +} + +function clampOpacity(value: unknown, fallback = 1) { + const parsed = toFiniteNumber(value, fallback) + if (parsed > 1) { + return Math.max(0, Math.min(1, parsed / 100)) + } + return Math.max(0, Math.min(1, parsed)) +} + +function toColorString(value: unknown): string | null { + if (typeof value === 'string' && value.trim()) + return value.trim() + return null +} + +function applyAlpha(hexColor: unknown, opacity = 1) { + if (typeof hexColor !== 'string') + return String(hexColor ?? '') + const normalized = hexColor.trim() + if (!HEX_COLOR_RE.test(normalized)) { + return normalized + } + + const hex = normalized.slice(1) + const expanded = hex.length === 3 + ? hex.split('').map(char => `${char}${char}`).join('') + : hex + const alpha = Math.round(Math.max(0, Math.min(1, opacity)) * 255).toString(16).padStart(2, '0') + return `#${expanded}${alpha}` +} + +function toCssShadow( + color: string | null, + offsetX: number, + offsetY: number, + blur: number, + enabled: boolean, + opacity?: number, + inset?: boolean, +) { + if (!enabled || !color) { + return 'none' + } + const finalColor = opacity !== undefined && opacity < 1 + ? applyAlpha(color, opacity) + : color + const insetStr = inset ? 'inset ' : '' + return `${insetStr}${offsetX}px ${offsetY}px ${Math.max(0, blur)}px ${finalColor}` +} + +function createBindingId() { + if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { + return crypto.randomUUID() + } + return `binding_${Date.now()}_${Math.random().toString(16).slice(2)}` +} + +function readBindingDefinition(binding: string | LayerBindingDefinition | undefined): LayerBindingDefinition | null { + if (!binding) { + return null + } + if (typeof binding === 'string') { + return { + id: createBindingId(), + type: 'variable', + value: binding, + variables: [binding], + priority: 0, + } + } + if ((binding.type === 'variable' || binding.type === 'expression') && typeof binding.value === 'string') { + return { + ...binding, + id: binding.id || createBindingId(), + priority: typeof binding.priority === 'number' && Number.isFinite(binding.priority) ? binding.priority : 0, + } + } + return null +} + +function readBindingDefinitions(binding: LayerBindingValue | undefined) { + if (Array.isArray(binding)) { + return binding + .map(item => readBindingDefinition(item)) + .filter(Boolean) as LayerBindingDefinition[] + } + + const normalized = readBindingDefinition(binding) + return normalized ? [normalized] : [] +} + +function pickActiveBinding(binding: LayerBindingValue | undefined) { + const definitions = readBindingDefinitions(binding) + let selected: LayerBindingDefinition | null = null + let selectedPriority = Number.NEGATIVE_INFINITY + + // 同一字段允许挂多条绑定规则,最终取优先级最高且值有效的一条。 + for (const definition of definitions) { + if (definition.enabled === false || !definition.value.trim()) { + continue + } + + const priority = typeof definition.priority === 'number' && Number.isFinite(definition.priority) + ? definition.priority + : 0 + + if (!selected || priority > selectedPriority) { + selected = definition + selectedPriority = priority + } + } + + return selected +} + +function buildExpressionScope(variableMap: Record) { + const vars: Record = {} + const modules: Record> = {} + + for (const variable of Object.values(variableMap)) { + vars[variable.path] = variable.value + modules[variable.moduleLabel] ||= {} + modules[variable.moduleLabel][variable.propLabel] = variable.value + } + + return { + // 同时暴露 vars 和模块对象两套访问方式,兼容简单变量和表达式场景。 + vars, + modules, + ...modules, + Math, + Number, + String, + Boolean, + Date, + } +} + +function evaluateExpression(expression: string, variableMap: Record) { + const scope = buildExpressionScope(variableMap) + return safeEvaluate(expression, scope) +} + +export function resolveLayerBindingValue(layer: Layer, field: string, variableMap: Record) { + const binding = pickActiveBinding(layer.bindings?.[field]) + if (!binding?.value?.trim()) { + return undefined + } + + if (binding.type === 'variable') { + return variableMap[binding.value]?.value + } + + // 表达式绑定允许按运行时变量动态计算展示值。 + return evaluateExpression(binding.value, variableMap) +} + +function readBoundOrLayerValue( + layer: Layer, + variableMap: Record, + field: string, + fallback?: T, +) { + const bindingValue = resolveLayerBindingValue(layer, field, variableMap) + if (bindingValue !== undefined) { + return bindingValue as T + } + + const layerRecord = layer as unknown as Record + const layerValue = get(layerRecord, field) + if (layerValue !== undefined) { + return layerValue as T + } + + return fallback as T +} + +export function resolveLayerDisplayValue(layer: Layer, variableMap: Record) { + const fallback = layer.type === 'text' + ? (layer.config?.content || '') + : layer.type === 'number' + ? layer.config?.defaultValue + : undefined + const rawValue = readBoundOrLayerValue(layer, variableMap, 'value', fallback) + + if (layer.type === 'number') { + const numericValue = toFiniteNumber(rawValue, 0) + const decimals = Math.max(0, Math.round(toFiniteNumber(layer.config?.decimals, 2))) + const formatter = new Intl.NumberFormat('zh-CN', { + minimumFractionDigits: decimals, + maximumFractionDigits: decimals, + useGrouping: false, + }) + const prefix = String(layer.config?.prefix || '') + const suffix = String(layer.config?.suffix || '') + return `${prefix}${formatter.format(numericValue)}${suffix}` + } + + if (rawValue === null || rawValue === undefined) { + return '' + } + + return String(rawValue) +} + +// 条件样式:按优先级排序后逐条检查,匹配的条件覆盖对应样式字段。 +function resolveConditionalStyles(layer: Layer, variableMap: Record): Record { + if (!layer.conditionalStyles?.length) + return {} + + const activeRules = layer.conditionalStyles + .filter(cs => cs.enabled !== false && cs.condition?.trim()) + .sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0)) + + const merged: Record = {} + for (const rule of activeRules) { + try { + const matched = evaluateExpression(rule.condition, variableMap) + if (matched) { + Object.assign(merged, rule.style) + } + } + catch { + // 条件表达式执行失败时跳过该规则 + } + } + // 向后兼容:旧数据使用 fillColor,新数据使用 backgroundColor + if (merged.fillColor !== undefined && merged.backgroundColor === undefined) { + merged.backgroundColor = merged.fillColor + delete merged.fillColor + } + return merged +} + +export function resolveLayerAppearance(layer: Layer, autoTextColor: string, variableMap: Record) { + // 外观解析统一兼容:绑定值 -> style.* -> config.* -> 默认值。 + const manualTextColor = toColorString(readBoundOrLayerValue(layer, variableMap, 'style.text.color')) + || toColorString(readBoundOrLayerValue(layer, variableMap, 'config.textColor')) + const textColor = manualTextColor || autoTextColor + + const fillOpacity = clampOpacity( + readBoundOrLayerValue( + layer, + variableMap, + 'style.fill.opacity', + readBoundOrLayerValue(layer, variableMap, 'config.fillOpacity', 100), + ), + ) + + const rawBackgroundColor = layer.type === 'rect' + ? toColorString(readBoundOrLayerValue(layer, variableMap, 'style.fill.color', readBoundOrLayerValue(layer, variableMap, 'config.fillColor', DEFAULT_RECT_FILL))) || DEFAULT_RECT_FILL + : toColorString(readBoundOrLayerValue(layer, variableMap, 'style.fill.color', readBoundOrLayerValue(layer, variableMap, 'config.fillColor'))) + let backgroundColor: string | null = rawBackgroundColor ? applyAlpha(rawBackgroundColor, fillOpacity) : null + + // 渐变填充:优先级高于纯色(有 activeType 时仅在 gradient 模式下生效) + const fillActiveType = layer.style?.fill?.activeType + const gradient = layer.style?.fill?.gradient + if (gradient?.type && gradient.colors?.length >= 2 && (!fillActiveType || fillActiveType === 'gradient')) { + const stops = gradient.colors.map((c: string, i: number) => { + const pos = gradient.stops?.[i] ?? (i / Math.max(gradient.colors.length - 1, 1)) + return `${c} ${Math.round(pos * 100)}%` + }).join(', ') + backgroundColor = gradient.type === 'linear' + ? `linear-gradient(${gradient.angle ?? 180}deg, ${stops})` + : `radial-gradient(circle, ${stops})` + } + + const borderColor = toColorString(readBoundOrLayerValue(layer, variableMap, 'style.border.color')) + const borderWidth = Math.max( + 0, + toFiniteNumber(readBoundOrLayerValue(layer, variableMap, 'style.border.width', 0), 0), + ) + const borderStyle = (readBoundOrLayerValue(layer, variableMap, 'style.border.style', DEFAULT_BORDER_STYLE) || DEFAULT_BORDER_STYLE) as 'solid' | 'dashed' | 'dotted' | 'none' + const borderRadius = Math.max( + 0, + toFiniteNumber( + readBoundOrLayerValue( + layer, + variableMap, + 'style.border.radius', + readBoundOrLayerValue(layer, variableMap, 'config.radius', 0), + ), + 0, + ), + ) + + const shadowColor = toColorString(readBoundOrLayerValue(layer, variableMap, 'style.shadow.color')) + const shadowBlur = toFiniteNumber(readBoundOrLayerValue(layer, variableMap, 'style.shadow.blur', 0), 0) + const shadowOffsetX = toFiniteNumber(readBoundOrLayerValue(layer, variableMap, 'style.shadow.offsetX', 0), 0) + const shadowOffsetY = toFiniteNumber(readBoundOrLayerValue(layer, variableMap, 'style.shadow.offsetY', 0), 0) + const shadowEnabled = Boolean( + readBoundOrLayerValue( + layer, + variableMap, + 'style.shadow.enabled', + Boolean(shadowColor && (shadowBlur > 0 || shadowOffsetX !== 0 || shadowOffsetY !== 0)), + ), + ) + const shadowOpacity = clampOpacity( + readBoundOrLayerValue(layer, variableMap, 'style.shadow.opacity', 1), + 1, + ) + const shadowInset = Boolean( + readBoundOrLayerValue(layer, variableMap, 'style.shadow.inset', false), + ) + + const textShadowColor = toColorString(readBoundOrLayerValue(layer, variableMap, 'style.textShadow.color')) + const textShadowBlur = toFiniteNumber(readBoundOrLayerValue(layer, variableMap, 'style.textShadow.blur', 0), 0) + const textShadowOffsetX = toFiniteNumber(readBoundOrLayerValue(layer, variableMap, 'style.textShadow.offsetX', 0), 0) + const textShadowOffsetY = toFiniteNumber(readBoundOrLayerValue(layer, variableMap, 'style.textShadow.offsetY', 0), 0) + const textShadowEnabled = Boolean(readBoundOrLayerValue(layer, variableMap, 'style.textShadow.enabled', false)) + + const fontSize = Math.max( + 10, + toFiniteNumber( + readBoundOrLayerValue( + layer, + variableMap, + 'style.text.fontSize', + readBoundOrLayerValue(layer, variableMap, 'config.fontSize', layer.type === 'number' ? 24 : 18), + ), + layer.type === 'number' ? 24 : 18, + ), + ) + const fontWeight = readBoundOrLayerValue( + layer, + variableMap, + 'style.text.fontWeight', + readBoundOrLayerValue(layer, variableMap, 'config.fontWeight', layer.type === 'number' ? 700 : 500), + ) + const fontStyle = (readBoundOrLayerValue( + layer, + variableMap, + 'style.text.fontStyle', + readBoundOrLayerValue(layer, variableMap, 'config.fontStyle', 'normal'), + ) || 'normal') as 'normal' | 'italic' + + const textDecoration = (readBoundOrLayerValue( + layer, + variableMap, + 'style.text.textDecoration', + readBoundOrLayerValue(layer, variableMap, 'config.textDecoration', 'none'), + ) || 'none') as 'none' | 'underline' + + const textAlign = (readBoundOrLayerValue( + layer, + variableMap, + 'style.text.align', + readBoundOrLayerValue(layer, variableMap, 'config.align', 'center'), + ) || 'center') as 'left' | 'center' | 'right' + const verticalAlign = (readBoundOrLayerValue( + layer, + variableMap, + 'style.text.verticalAlign', + readBoundOrLayerValue(layer, variableMap, 'config.verticalAlign', 'middle'), + ) || 'middle') as 'top' | 'middle' | 'bottom' + + const appearance = { + displayValue: resolveLayerDisplayValue(layer, variableMap), + textColor, + backgroundColor, + borderColor, + borderWidth, + borderStyle, + borderRadius, + boxShadow: toCssShadow(shadowColor, shadowOffsetX, shadowOffsetY, shadowBlur, shadowEnabled, shadowOpacity, shadowInset), + fontSize, + fontWeight, + fontStyle, + textDecoration, + textAlign, + verticalAlign, + textShadow: textShadowEnabled && textShadowColor + ? `${textShadowOffsetX}px ${textShadowOffsetY}px ${Math.max(0, textShadowBlur)}px ${textShadowColor}` + : 'none', + fillImage: (!fillActiveType || fillActiveType === 'image') ? (layer.style?.fill?.image ?? null) : null, + opacity: undefined as number | undefined, + visible: undefined as boolean | undefined, + animationClass: undefined as string | undefined, + } + + // 条件样式覆盖:匹配到的规则属性会覆盖上面解析的基础值 + const overrides = resolveConditionalStyles(layer, variableMap) + if (overrides.textColor !== undefined) + appearance.textColor = String(overrides.textColor) + if (overrides.backgroundColor !== undefined) + appearance.backgroundColor = String(overrides.backgroundColor) + if (overrides.borderColor !== undefined) + appearance.borderColor = String(overrides.borderColor) + if (overrides.borderWidth !== undefined) + appearance.borderWidth = toFiniteNumber(overrides.borderWidth, borderWidth) + if (overrides.borderStyle !== undefined) + appearance.borderStyle = String(overrides.borderStyle) as typeof borderStyle + if (overrides.borderRadius !== undefined) + appearance.borderRadius = toFiniteNumber(overrides.borderRadius, borderRadius) + if (overrides.fontSize !== undefined) + appearance.fontSize = toFiniteNumber(overrides.fontSize, fontSize) + if (overrides.fontWeight !== undefined) + appearance.fontWeight = overrides.fontWeight as string | number + if (overrides.fontStyle !== undefined) + appearance.fontStyle = String(overrides.fontStyle) as typeof fontStyle + if (overrides.textDecoration !== undefined) + appearance.textDecoration = String(overrides.textDecoration) as typeof textDecoration + if (overrides.opacity !== undefined) { + appearance.opacity = clampOpacity(overrides.opacity) + } + if (overrides.visible !== undefined) { + appearance.visible = Boolean(overrides.visible) + } + if (overrides.blink) { + appearance.animationClass = 'dcs-blink' + } + return appearance +} diff --git a/packages/core/src/components/editor/components/text.vue b/packages/core/src/components/editor/components/text.vue new file mode 100644 index 0000000..1ca3a70 --- /dev/null +++ b/packages/core/src/components/editor/components/text.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/packages/core/src/components/editor/components/types.ts b/packages/core/src/components/editor/components/types.ts new file mode 100644 index 0000000..a222d63 --- /dev/null +++ b/packages/core/src/components/editor/components/types.ts @@ -0,0 +1,44 @@ +import type { Layer } from '@cslab-dcs/schema' +import type { ComputedRef, InjectionKey, Ref } from 'vue' +import { inject, provide } from 'vue' + +export interface EditorComponentResolvedState { + displayValue: string + textColor: string + backgroundColor: string | null + borderColor: string | null + borderWidth: number + borderStyle: 'solid' | 'dashed' | 'dotted' | 'none' + borderRadius: number + boxShadow: string + fontSize: number + fontWeight: string | number + fontStyle: 'normal' | 'italic' + textDecoration: 'none' | 'underline' + textAlign: 'left' | 'center' | 'right' + verticalAlign: 'top' | 'middle' | 'bottom' + textShadow: string + fillImage: { src: string, fit?: string, opacity?: number } | null + opacity?: number + visible?: boolean + animationClass?: string +} + +export interface EditorComponentLayerContext { + layer: Ref + scale: ComputedRef + isSelected: Ref + resolved: ComputedRef + editing: Ref + onEditComplete: () => void +} + +const EditorComponentLayerKey: InjectionKey = Symbol('editor-component-layer') + +export function provideEditorComponentLayerContext(state: EditorComponentLayerContext) { + provide(EditorComponentLayerKey, state) +} + +export function useEditorComponentLayerInject() { + return inject(EditorComponentLayerKey)! +} diff --git a/packages/core/src/components/editor/components/valve-controller.vue b/packages/core/src/components/editor/components/valve-controller.vue new file mode 100644 index 0000000..37ee88b --- /dev/null +++ b/packages/core/src/components/editor/components/valve-controller.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/packages/core/src/components/editor/console-panel/index.vue b/packages/core/src/components/editor/console-panel/index.vue new file mode 100644 index 0000000..5997414 --- /dev/null +++ b/packages/core/src/components/editor/console-panel/index.vue @@ -0,0 +1,281 @@ + + + + + diff --git a/packages/core/src/components/editor/controls/EdColorInput.vue b/packages/core/src/components/editor/controls/EdColorInput.vue new file mode 100644 index 0000000..f600aac --- /dev/null +++ b/packages/core/src/components/editor/controls/EdColorInput.vue @@ -0,0 +1,192 @@ + + + + + diff --git a/packages/core/src/components/editor/controls/EdIconButton.vue b/packages/core/src/components/editor/controls/EdIconButton.vue new file mode 100644 index 0000000..5867e77 --- /dev/null +++ b/packages/core/src/components/editor/controls/EdIconButton.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/packages/core/src/components/editor/controls/EdInput.vue b/packages/core/src/components/editor/controls/EdInput.vue new file mode 100644 index 0000000..1a408b3 --- /dev/null +++ b/packages/core/src/components/editor/controls/EdInput.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/packages/core/src/components/editor/controls/EdNumberInput.vue b/packages/core/src/components/editor/controls/EdNumberInput.vue new file mode 100644 index 0000000..bdfc180 --- /dev/null +++ b/packages/core/src/components/editor/controls/EdNumberInput.vue @@ -0,0 +1,229 @@ + + + + + diff --git a/packages/core/src/components/editor/controls/EdSection.vue b/packages/core/src/components/editor/controls/EdSection.vue new file mode 100644 index 0000000..a8029cf --- /dev/null +++ b/packages/core/src/components/editor/controls/EdSection.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/packages/core/src/components/editor/controls/EdSegmented.vue b/packages/core/src/components/editor/controls/EdSegmented.vue new file mode 100644 index 0000000..32b7fc3 --- /dev/null +++ b/packages/core/src/components/editor/controls/EdSegmented.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/packages/core/src/components/editor/controls/EdSelect.vue b/packages/core/src/components/editor/controls/EdSelect.vue new file mode 100644 index 0000000..c8f2e2e --- /dev/null +++ b/packages/core/src/components/editor/controls/EdSelect.vue @@ -0,0 +1,317 @@ + + + + + + + diff --git a/packages/core/src/components/editor/controls/EdToggle.vue b/packages/core/src/components/editor/controls/EdToggle.vue new file mode 100644 index 0000000..a5745dd --- /dev/null +++ b/packages/core/src/components/editor/controls/EdToggle.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/packages/core/src/components/editor/controls/index.ts b/packages/core/src/components/editor/controls/index.ts new file mode 100644 index 0000000..d384820 --- /dev/null +++ b/packages/core/src/components/editor/controls/index.ts @@ -0,0 +1,11 @@ +export { default as EdColorInput } from './EdColorInput.vue' +export { default as EdIconButton } from './EdIconButton.vue' +export { default as EdInput } from './EdInput.vue' +export { default as EdNumberInput } from './EdNumberInput.vue' +export { default as EdSection } from './EdSection.vue' +export { default as EdSegmented } from './EdSegmented.vue' +export type { EdSegmentedOption } from './EdSegmented.vue' +export { default as EdSelect } from './EdSelect.vue' +export type { EdSelectOption } from './EdSelect.vue' + +export { default as EdToggle } from './EdToggle.vue' diff --git a/packages/core/src/components/editor/header/constants.ts b/packages/core/src/components/editor/header/constants.ts new file mode 100644 index 0000000..9be457c --- /dev/null +++ b/packages/core/src/components/editor/header/constants.ts @@ -0,0 +1,17 @@ +import { MAX_CANVAS_ZOOM_PERCENT } from '@/constants' + +export interface HeaderMenuAction { + label: string + command: string + divided?: boolean + disabled?: boolean +} + +export const HEADER_PROJECT_ACTIONS: HeaderMenuAction[] = [ + { label: '导出', command: 'export', divided: true }, + { label: '创建副本', command: 'duplicate' }, + { label: '重命名', command: 'rename' }, + { label: '查看历史版本', command: 'history', disabled: true }, +] + +export const HEADER_ZOOM_OPTIONS = [10, 25, 50, 75, 100, 125, 150, 200, MAX_CANVAS_ZOOM_PERCENT] as const diff --git a/packages/core/src/components/editor/header/index.vue b/packages/core/src/components/editor/header/index.vue new file mode 100644 index 0000000..80834dc --- /dev/null +++ b/packages/core/src/components/editor/header/index.vue @@ -0,0 +1,262 @@ + + + + + diff --git a/packages/core/src/components/editor/header/main-menu-dropdown.vue b/packages/core/src/components/editor/header/main-menu-dropdown.vue new file mode 100644 index 0000000..4f9d8be --- /dev/null +++ b/packages/core/src/components/editor/header/main-menu-dropdown.vue @@ -0,0 +1,163 @@ + + + + + diff --git a/packages/core/src/components/editor/header/project-name-dropdown.vue b/packages/core/src/components/editor/header/project-name-dropdown.vue new file mode 100644 index 0000000..f006590 --- /dev/null +++ b/packages/core/src/components/editor/header/project-name-dropdown.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/packages/core/src/components/editor/header/right-actions.vue b/packages/core/src/components/editor/header/right-actions.vue new file mode 100644 index 0000000..c14b03e --- /dev/null +++ b/packages/core/src/components/editor/header/right-actions.vue @@ -0,0 +1,169 @@ + + + + + diff --git a/packages/core/src/components/editor/header/zoom-dropdown.vue b/packages/core/src/components/editor/header/zoom-dropdown.vue new file mode 100644 index 0000000..9c8dbca --- /dev/null +++ b/packages/core/src/components/editor/header/zoom-dropdown.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/packages/core/src/components/editor/index.ts b/packages/core/src/components/editor/index.ts new file mode 100644 index 0000000..c74a6e3 --- /dev/null +++ b/packages/core/src/components/editor/index.ts @@ -0,0 +1,2 @@ +export { default as CanvasEditor } from './canvas/index.vue' +export { default as EditorHeader } from './header/index.vue' diff --git a/packages/core/src/components/editor/pid-controller-panel/PidPanelBars.vue b/packages/core/src/components/editor/pid-controller-panel/PidPanelBars.vue new file mode 100644 index 0000000..8d71fa4 --- /dev/null +++ b/packages/core/src/components/editor/pid-controller-panel/PidPanelBars.vue @@ -0,0 +1,157 @@ + + + + + diff --git a/packages/core/src/components/editor/pid-controller-panel/PidPanelData.vue b/packages/core/src/components/editor/pid-controller-panel/PidPanelData.vue new file mode 100644 index 0000000..a39efb3 --- /dev/null +++ b/packages/core/src/components/editor/pid-controller-panel/PidPanelData.vue @@ -0,0 +1,166 @@ + + + + + diff --git a/packages/core/src/components/editor/pid-controller-panel/PidPanelFooter.vue b/packages/core/src/components/editor/pid-controller-panel/PidPanelFooter.vue new file mode 100644 index 0000000..ab3f5ce --- /dev/null +++ b/packages/core/src/components/editor/pid-controller-panel/PidPanelFooter.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/packages/core/src/components/editor/pid-controller-panel/PidPanelHeader.vue b/packages/core/src/components/editor/pid-controller-panel/PidPanelHeader.vue new file mode 100644 index 0000000..d5be111 --- /dev/null +++ b/packages/core/src/components/editor/pid-controller-panel/PidPanelHeader.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/packages/core/src/components/editor/pid-controller-panel/PidPanelMode.vue b/packages/core/src/components/editor/pid-controller-panel/PidPanelMode.vue new file mode 100644 index 0000000..42616c0 --- /dev/null +++ b/packages/core/src/components/editor/pid-controller-panel/PidPanelMode.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/packages/core/src/components/editor/pid-controller-panel/PidPanelParams.vue b/packages/core/src/components/editor/pid-controller-panel/PidPanelParams.vue new file mode 100644 index 0000000..9e77349 --- /dev/null +++ b/packages/core/src/components/editor/pid-controller-panel/PidPanelParams.vue @@ -0,0 +1,160 @@ + + + + + diff --git a/packages/core/src/components/editor/pid-controller-panel/PidPanelToolbar.vue b/packages/core/src/components/editor/pid-controller-panel/PidPanelToolbar.vue new file mode 100644 index 0000000..6ddedb9 --- /dev/null +++ b/packages/core/src/components/editor/pid-controller-panel/PidPanelToolbar.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/packages/core/src/components/editor/pid-controller-panel/emitter.ts b/packages/core/src/components/editor/pid-controller-panel/emitter.ts new file mode 100644 index 0000000..e001c74 --- /dev/null +++ b/packages/core/src/components/editor/pid-controller-panel/emitter.ts @@ -0,0 +1,10 @@ +import mitt from 'mitt' + +// eslint-disable-next-line ts/consistent-type-definitions +export type PidControllerWriteEvents = { + 'pid:setSV': { layerId: string, variablePath: string, value: number } + 'pid:setMode': { layerId: string, variablePath: string, mode: string } + 'pid:setParams': { layerId: string, variablePath: string, params: { Kp?: number, Ki?: number, Kd?: number } } +} + +export const pidControllerEmitter = mitt() diff --git a/packages/core/src/components/editor/pid-controller-panel/index.vue b/packages/core/src/components/editor/pid-controller-panel/index.vue new file mode 100644 index 0000000..c3b0899 --- /dev/null +++ b/packages/core/src/components/editor/pid-controller-panel/index.vue @@ -0,0 +1,232 @@ + + + + + diff --git a/packages/core/src/components/editor/pid-controller-panel/types.ts b/packages/core/src/components/editor/pid-controller-panel/types.ts new file mode 100644 index 0000000..e6720f2 --- /dev/null +++ b/packages/core/src/components/editor/pid-controller-panel/types.ts @@ -0,0 +1,150 @@ +import type { Layer } from '@cslab-dcs/schema' +import type { CanvasRuntimeVariable } from '../canvas/context/runtime' + +/** 面板实例配置,由 runtime.vue 传入 */ +export interface PidPanelInstance { + /** 图层 ID,用于去重(同一图层只打开一个面板) */ + layerId: string + /** 图层引用,读取 tagNumber / bindings / config */ + layer: Layer + /** 是否钉住 */ + pinned: boolean + /** 面板位置 */ + x: number + y: number +} + +/** 从 variableMap 中解析出的面板展示数据 */ +export interface PidPanelData { + pv: number | null + sp: number | null + op: number | null + mode: string | null + /** 量程 */ + rangeH: number | null + rangeL: number | null + /** 输出限 */ + cvH: number | null + cvL: number | null + /** PID 参数 */ + kp: number | null + ki: number | null + kd: number | null + kpMin: number | null + kpMax: number | null + kiMin: number | null + kiMax: number | null + kdMin: number | null + kdMax: number | null + /** 单位 */ + spUnit: string + /** 设备描述(优先从 config.description,回退到 Type_M) */ + description: string + /** PV/SV 小数位数 */ + decimalPlaces: number + /** OP 小数位数 */ + opDecimalPlaces: number +} + +/** 模式值映射 */ +export const MODE_MAP: Record = { + 手动: 'MAN', + 自动: 'AUTO', + 串级: 'CAS', + MAN: 'MAN', + AUTO: 'AUTO', + CAS: 'CAS', +} + +export const MODE_LIST = ['MAN', 'AUTO', 'CAS'] as const +export type PidMode = typeof MODE_LIST[number] + +/** + * 提取图层关联的模块前缀。 + * 优先使用 tagNumber(如 "LIC001"),它与 WebSocket moduleDataMap 的 key 一致。 + * 回退到从 bindings.pv 路径中解析(如 "LIC001.PV" → "LIC001")。 + */ +export function getModulePrefix(layer: Layer): string { + // tagNumber 直接就是模块标签,与 WebSocket 数据 key 对应 + const tag = layer.tagNumber?.trim() + if (tag) + return tag + + // 回退:从 pv 绑定路径提取 + const pvBinding = layer.bindings?.pv + const bindingValue = Array.isArray(pvBinding) ? pvBinding[0] : pvBinding + const rawPath = typeof bindingValue === 'string' ? bindingValue : bindingValue?.value ?? '' + const dotIndex = rawPath.indexOf('.') + return dotIndex > 0 ? rawPath.slice(0, dotIndex) : '' +} + +/** + * 从 variableMap 中读取面板需要的所有数据。 + * 变量绑定路径格式为 moduleLabel.propLabel,例如 "PIC001.PV"。 + * 面板需要的字段使用统一前缀(layer.bindings 中绑定的 moduleLabel)拼接。 + */ +export function resolvePidPanelData( + layer: Layer, + variableMap: Record, +): PidPanelData { + const modulePrefix = getModulePrefix(layer) + + function readNum(field: string): number | null { + if (!modulePrefix) + return null + const v = variableMap[`${modulePrefix}.${field}`]?.value + if (typeof v === 'number') + return v + if (typeof v === 'string') { + const n = Number(v) + return Number.isFinite(n) ? n : null + } + return null + } + + function readStr(field: string): string | null { + if (!modulePrefix) + return null + const v = variableMap[`${modulePrefix}.${field}`]?.value + return v != null ? String(v) : null + } + + const modeRaw = readStr('MODE') + const mode = modeRaw ? (MODE_MAP[modeRaw] ?? null) : null + + // 单位回退链:SP → PV → H,取第一个有单位的字段 + const spUnit = modulePrefix + ? (variableMap[`${modulePrefix}.SP`]?.unit + ?? variableMap[`${modulePrefix}.PV`]?.unit + ?? variableMap[`${modulePrefix}.H`]?.unit + ?? '') + : '' + + // 描述:优先用 config 中手动填写的,回退到 WebSocket 的 Type_M + const configDesc = (layer.config?.description as string)?.trim() ?? '' + const typeM = readStr('Type_M') ?? '' + + return { + pv: readNum('PV'), + sp: readNum('SP'), + op: readNum('OP'), + mode, + rangeH: readNum('H'), + rangeL: readNum('L'), + cvH: readNum('CV_H'), + cvL: readNum('CV_L'), + kp: readNum('Kp'), + ki: readNum('Ki'), + kd: readNum('Kd'), + kpMin: readNum('Kp_min'), + kpMax: readNum('Kp_max'), + kiMin: readNum('Ki_min'), + kiMax: readNum('Ki_max'), + kdMin: readNum('Kd_min'), + kdMax: readNum('Kd_max'), + spUnit, + description: configDesc || typeM, + decimalPlaces: Number(layer.config?.decimalPlaces ?? 2), + opDecimalPlaces: Number(layer.config?.opDecimalPlaces ?? 2), + } +} diff --git a/packages/core/src/components/editor/preview-overlay/index.vue b/packages/core/src/components/editor/preview-overlay/index.vue new file mode 100644 index 0000000..67e8be5 --- /dev/null +++ b/packages/core/src/components/editor/preview-overlay/index.vue @@ -0,0 +1,384 @@ + + + + + diff --git a/packages/core/src/components/editor/runtime-console/index.vue b/packages/core/src/components/editor/runtime-console/index.vue new file mode 100644 index 0000000..16d3419 --- /dev/null +++ b/packages/core/src/components/editor/runtime-console/index.vue @@ -0,0 +1,400 @@ + + + + + diff --git a/packages/core/src/components/editor/runtime-dialog/index.vue b/packages/core/src/components/editor/runtime-dialog/index.vue new file mode 100644 index 0000000..ff41108 --- /dev/null +++ b/packages/core/src/components/editor/runtime-dialog/index.vue @@ -0,0 +1,252 @@ + + + + + diff --git a/packages/core/src/components/editor/runtime-manual-control.vue b/packages/core/src/components/editor/runtime-manual-control.vue new file mode 100644 index 0000000..4a957b9 --- /dev/null +++ b/packages/core/src/components/editor/runtime-manual-control.vue @@ -0,0 +1,465 @@ + + + + + diff --git a/packages/core/src/components/editor/runtime-nav-bar.vue b/packages/core/src/components/editor/runtime-nav-bar.vue new file mode 100644 index 0000000..19310fc --- /dev/null +++ b/packages/core/src/components/editor/runtime-nav-bar.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/packages/core/src/components/editor/status-bar/index.vue b/packages/core/src/components/editor/status-bar/index.vue new file mode 100644 index 0000000..0799da7 --- /dev/null +++ b/packages/core/src/components/editor/status-bar/index.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/packages/core/src/components/editor/styles/editor-tokens.css b/packages/core/src/components/editor/styles/editor-tokens.css new file mode 100644 index 0000000..294385e --- /dev/null +++ b/packages/core/src/components/editor/styles/editor-tokens.css @@ -0,0 +1,73 @@ +:root { + /* ── Background ── */ + --ed-bg-deep: #f0f0f0; + --ed-bg-surface: #f7f7f8; + --ed-bg-raised: #ffffff; + --ed-bg-hover: #ebebeb; + --ed-bg-active: #e0e0e0; + --ed-bg-selected: #daebf7; + + /* ── Text ── */ + --ed-text-primary: #1a1a1a; + --ed-text-secondary: #6b6b6b; + --ed-text-tertiary: #999999; + --ed-text-disabled: #c4c4c4; + + /* ── Accent ── */ + --ed-accent: #1677ff; + --ed-accent-hover: #4096ff; + --ed-accent-subtle: #e6f0ff; + + /* ── Functional ── */ + --ed-danger: #f5222d; + --ed-success: #0ac05e; + --ed-warning: #faad14; + + /* ── Border ── */ + --ed-border: #e2e2e2; + --ed-border-strong: #d0d0d0; + --ed-border-focus: #1677ff; + + /* ── Spacing ── */ + --ed-space-1: 2px; + --ed-space-2: 4px; + --ed-space-3: 6px; + --ed-space-4: 8px; + --ed-space-5: 12px; + --ed-space-6: 16px; + --ed-space-7: 20px; + --ed-space-8: 24px; + + /* ── Font ── */ + --ed-font: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif; + --ed-font-xs: 11px; + --ed-font-sm: 12px; + --ed-font-md: 13px; + --ed-font-lg: 14px; + --ed-font-weight-normal: 400; + --ed-font-weight-medium: 500; + --ed-font-weight-semibold: 600; + + /* ── Radius ── */ + --ed-radius-sm: 4px; + --ed-radius-md: 6px; + --ed-radius-lg: 8px; + + /* ── Shadow ── */ + --ed-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08); + --ed-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); + --ed-shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12); + + /* ── Size ── */ + --ed-control-height: 28px; + --ed-control-height-sm: 24px; + --ed-row-height: 30px; + --ed-header-height: 40px; + --ed-sidebar-width: 240px; + --ed-toolbar-width: 40px; + --ed-statusbar-height: 24px; + + /* ── Transition ── */ + --ed-transition-fast: 0.12s ease; + --ed-transition-normal: 0.2s ease; +} diff --git a/packages/core/src/components/editor/valve-controller-panel/ValvePanelControl.vue b/packages/core/src/components/editor/valve-controller-panel/ValvePanelControl.vue new file mode 100644 index 0000000..f117c7b --- /dev/null +++ b/packages/core/src/components/editor/valve-controller-panel/ValvePanelControl.vue @@ -0,0 +1,206 @@ + + + + + diff --git a/packages/core/src/components/editor/valve-controller-panel/ValvePanelDisplay.vue b/packages/core/src/components/editor/valve-controller-panel/ValvePanelDisplay.vue new file mode 100644 index 0000000..6ea6598 --- /dev/null +++ b/packages/core/src/components/editor/valve-controller-panel/ValvePanelDisplay.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/packages/core/src/components/editor/valve-controller-panel/ValvePanelHeader.vue b/packages/core/src/components/editor/valve-controller-panel/ValvePanelHeader.vue new file mode 100644 index 0000000..ea22ed7 --- /dev/null +++ b/packages/core/src/components/editor/valve-controller-panel/ValvePanelHeader.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/packages/core/src/components/editor/valve-controller-panel/emitter.ts b/packages/core/src/components/editor/valve-controller-panel/emitter.ts new file mode 100644 index 0000000..77234c2 --- /dev/null +++ b/packages/core/src/components/editor/valve-controller-panel/emitter.ts @@ -0,0 +1,8 @@ +import mitt from 'mitt' + +// eslint-disable-next-line ts/consistent-type-definitions +export type ValveControllerWriteEvents = { + 'valve:setOP': { layerId: string, variablePath: string, value: number } +} + +export const valveControllerEmitter = mitt() diff --git a/packages/core/src/components/editor/valve-controller-panel/index.vue b/packages/core/src/components/editor/valve-controller-panel/index.vue new file mode 100644 index 0000000..cfd326a --- /dev/null +++ b/packages/core/src/components/editor/valve-controller-panel/index.vue @@ -0,0 +1,173 @@ + + + + + diff --git a/packages/core/src/components/editor/valve-controller-panel/types.ts b/packages/core/src/components/editor/valve-controller-panel/types.ts new file mode 100644 index 0000000..b691ac8 --- /dev/null +++ b/packages/core/src/components/editor/valve-controller-panel/types.ts @@ -0,0 +1,65 @@ +import type { Layer } from '@cslab-dcs/schema' +import type { CanvasRuntimeVariable } from '../canvas/context/runtime' + +export interface ValvePanelInstance { + layerId: string + layer: Layer + pinned: boolean + x: number + y: number +} + +export interface ValvePanelData { + op: number | null + valveType: string + description: string + decimalPlaces: number +} + +export function getModulePrefix(layer: Layer): string { + const tag = layer.tagNumber?.trim() + if (tag) + return tag + const opBinding = layer.bindings?.op + const bindingValue = Array.isArray(opBinding) ? opBinding[0] : opBinding + const rawPath = typeof bindingValue === 'string' ? bindingValue : bindingValue?.value ?? '' + const dotIndex = rawPath.indexOf('.') + return dotIndex > 0 ? rawPath.slice(0, dotIndex) : '' +} + +export function resolveValvePanelData( + layer: Layer, + variableMap: Record, +): ValvePanelData { + const modulePrefix = getModulePrefix(layer) + + function readNum(field: string): number | null { + if (!modulePrefix) + return null + const v = variableMap[`${modulePrefix}.${field}`]?.value + if (typeof v === 'number') + return v + if (typeof v === 'string') { + const n = Number(v) + return Number.isFinite(n) ? n : null + } + return null + } + + function readStr(field: string): string | null { + if (!modulePrefix) + return null + const v = variableMap[`${modulePrefix}.${field}`]?.value + return v != null ? String(v) : null + } + + const configDesc = (layer.config?.description as string)?.trim() ?? '' + const valveType = readStr('Val_type') ?? '' + + return { + op: readNum('OP'), + valveType, + description: configDesc || valveType, + decimalPlaces: Number(layer.config?.decimalPlaces ?? 2), + } +} diff --git a/packages/core/src/composables/useComponentTemplates.ts b/packages/core/src/composables/useComponentTemplates.ts new file mode 100644 index 0000000..cb4aac2 --- /dev/null +++ b/packages/core/src/composables/useComponentTemplates.ts @@ -0,0 +1,67 @@ +import type { Layer } from '@cslab-dcs/schema' +import { useLocalStorage } from '@vueuse/core' +import { computed } from 'vue' + +export interface ComponentTemplate { + id: string + name: string + layers: Layer[] + createdAt: number +} + +const STORAGE_KEY = 'DCS_COMPONENT_TEMPLATES' + +export function useComponentTemplates() { + const templates = useLocalStorage(STORAGE_KEY, []) + + const sortedTemplates = computed(() => + templates.value.toSorted((a, b) => b.createdAt - a.createdAt), + ) + + function createId() { + return `tpl_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` + } + + function saveTemplate(name: string, layers: Layer[]) { + if (!layers.length) + return null + + // 深拷贝图层数据,生成新 ID 避免冲突 + const clonedLayers = JSON.parse(JSON.stringify(layers)) as Layer[] + + const template: ComponentTemplate = { + id: createId(), + name: name.trim() || `模板 ${templates.value.length + 1}`, + layers: clonedLayers, + createdAt: Date.now(), + } + + templates.value = [...templates.value, template] + return template + } + + function removeTemplate(id: string) { + templates.value = templates.value.filter(t => t.id !== id) + } + + function renameTemplate(id: string, name: string) { + const index = templates.value.findIndex(t => t.id === id) + if (index >= 0) { + const updated = [...templates.value] + updated[index] = { ...updated[index], name: name.trim() } + templates.value = updated + } + } + + function getTemplate(id: string) { + return templates.value.find(t => t.id === id) ?? null + } + + return { + templates: sortedTemplates, + saveTemplate, + removeTemplate, + renameTemplate, + getTemplate, + } +} diff --git a/packages/core/src/composables/useDataConnection.ts b/packages/core/src/composables/useDataConnection.ts new file mode 100644 index 0000000..cd6dc6b --- /dev/null +++ b/packages/core/src/composables/useDataConnection.ts @@ -0,0 +1,375 @@ +import { ref, shallowRef } from 'vue' + +export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'error' +export type ConnectionMode = 'mock' | 'websocket' + +export interface MockVariable { + path: string + value: unknown + type: 'analog' | 'digital' | 'string' | 'enum' + quality: 'GOOD' | 'BAD' | 'UNCERTAIN' + seq?: number + timestamp?: string + source?: string + traceId?: string +} + +export interface WriteCommand { + path: string + value: unknown + traceId: string +} + +export interface WriteAck { + traceId: string + success: boolean + message?: string +} + +// 模块级状态(单例共享) +const connectionState = ref('disconnected') +const connectionMode = ref('mock') +const mockVariables = shallowRef([]) +const pushInterval = ref(1000) +const wsUrl = ref('') +const lastPushAt = ref(0) +const isDataStale = ref(false) +const STALE_THRESHOLD_MS = 10000 + +let intervalId: ReturnType | null = null +let onPushCallback: ((variables: MockVariable[]) => void) | null = null + +let connectTimer: ReturnType | null = null +let pushSeq = 0 +let staleCheckTimer: ReturnType | null = null + +// WebSocket 相关状态 +let ws: WebSocket | null = null +let heartbeatTimer: ReturnType | null = null +let reconnectTimer: ReturnType | null = null +let reconnectAttempts = 0 +const MAX_RECONNECT_ATTEMPTS = 5 +const RECONNECT_BASE_DELAY = 1000 +const HEARTBEAT_INTERVAL = 30000 +const pendingWrites = new Map void, timer: ReturnType }>() + +function generateTraceId() { + return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}` +} + +function startStaleCheck() { + stopStaleCheck() + staleCheckTimer = setInterval(() => { + if (connectionState.value === 'connected' && lastPushAt.value > 0) { + isDataStale.value = Date.now() - lastPushAt.value > STALE_THRESHOLD_MS + } + }, 2000) +} + +function stopStaleCheck() { + if (staleCheckTimer) { + clearInterval(staleCheckTimer) + staleCheckTimer = null + } +} + +// ================== Mock 模式 ================== + +function connectMock() { + connectionState.value = 'connecting' + connectTimer = setTimeout(() => { + connectTimer = null + connectionState.value = 'connected' + startStaleCheck() + startMockPushing() + }, 300) +} + +function disconnectMock() { + if (connectTimer) { + clearTimeout(connectTimer) + connectTimer = null + } + stopMockPushing() +} + +function startMockPushing() { + stopMockPushing() + pushSeq = 0 + intervalId = setInterval(() => { + if (onPushCallback && mockVariables.value.length > 0) { + pushSeq++ + const timestamp = new Date().toISOString() + const traceId = generateTraceId() + const stamped = mockVariables.value.map(v => ({ + ...v, + seq: pushSeq, + timestamp, + source: v.source || 'mock', + traceId, + })) + lastPushAt.value = Date.now() + isDataStale.value = false + onPushCallback(stamped) + } + }, pushInterval.value) +} + +function stopMockPushing() { + if (intervalId) { + clearInterval(intervalId) + intervalId = null + } +} + +// ================== WebSocket 模式 ================== + +function connectWebSocket() { + if (!wsUrl.value) { + connectionState.value = 'error' + return + } + + connectionState.value = 'connecting' + try { + ws = new WebSocket(wsUrl.value) + } + catch { + connectionState.value = 'error' + scheduleReconnect() + return + } + + ws.onopen = () => { + connectionState.value = 'connected' + reconnectAttempts = 0 + startHeartbeat() + startStaleCheck() + } + + ws.onmessage = (event) => { + try { + const data = JSON.parse(event.data) + + // 心跳响应 + if (data.type === 'pong') + return + + // 写入 ACK + if (data.type === 'writeAck' && data.traceId) { + const pending = pendingWrites.get(data.traceId) + if (pending) { + clearTimeout(pending.timer) + pendingWrites.delete(data.traceId) + pending.resolve({ traceId: data.traceId, success: data.success !== false, message: data.message }) + } + return + } + + // 变量推送 + if (data.type === 'push' && Array.isArray(data.variables)) { + lastPushAt.value = Date.now() + isDataStale.value = false + if (onPushCallback) { + onPushCallback(data.variables as MockVariable[]) + } + } + } + catch { + // 非 JSON 消息忽略 + } + } + + ws.onerror = () => { + connectionState.value = 'error' + isDataStale.value = true + } + + ws.onclose = () => { + stopHeartbeat() + if (connectionState.value !== 'disconnected') { + connectionState.value = 'error' + isDataStale.value = true + scheduleReconnect() + } + } +} + +function disconnectWebSocket() { + stopHeartbeat() + cancelReconnect() + // 清理所有 pending 写入 + for (const [id, pending] of pendingWrites) { + clearTimeout(pending.timer) + pending.resolve({ traceId: id, success: false, message: '连接已断开' }) + } + pendingWrites.clear() + + if (ws) { + ws.onclose = null + ws.close() + ws = null + } +} + +function startHeartbeat() { + stopHeartbeat() + heartbeatTimer = setInterval(() => { + if (ws?.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: 'ping' })) + } + }, HEARTBEAT_INTERVAL) +} + +function stopHeartbeat() { + if (heartbeatTimer) { + clearInterval(heartbeatTimer) + heartbeatTimer = null + } +} + +function scheduleReconnect() { + cancelReconnect() + if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) + return + + const delay = RECONNECT_BASE_DELAY * (2 ** reconnectAttempts) + reconnectAttempts++ + reconnectTimer = setTimeout(() => { + reconnectTimer = null + if (connectionState.value !== 'disconnected') { + connectWebSocket() + } + }, delay) +} + +function cancelReconnect() { + if (reconnectTimer) { + clearTimeout(reconnectTimer) + reconnectTimer = null + } +} + +// ================== 写入 + ACK ================== + +const WRITE_ACK_TIMEOUT = 5000 + +function writeVariable(path: string, value: unknown): Promise { + const traceId = generateTraceId() + + // Mock 模式直接返回成功 + if (connectionMode.value === 'mock') { + updateVariable(path, value) + return Promise.resolve({ traceId, success: true }) + } + + // WebSocket 模式发送写入命令并等待 ACK + if (!ws || ws.readyState !== WebSocket.OPEN) { + return Promise.resolve({ traceId, success: false, message: '连接未建立' }) + } + + return new Promise((resolve) => { + const timer = setTimeout(() => { + pendingWrites.delete(traceId) + resolve({ traceId, success: false, message: '写入超时' }) + }, WRITE_ACK_TIMEOUT) + + pendingWrites.set(traceId, { resolve, timer }) + ws!.send(JSON.stringify({ type: 'write', path, value, traceId })) + }) +} + +async function writeVariableWithConfirm( + path: string, + value: unknown, + options?: { message?: string, skipConfirm?: boolean }, +): Promise { + if (!options?.skipConfirm) { + const { ElMessageBox } = await import('element-plus') + try { + await ElMessageBox.confirm( + options?.message || `确认将 ${path} 写入值 ${String(value).length > 50 ? `${String(value).slice(0, 50)}...` : String(value)}?`, + '写入确认', + { + confirmButtonText: '确认写入', + cancelButtonText: '取消', + type: 'warning', + }, + ) + } + catch { + return { traceId: '', success: false, message: '用户取消' } + } + } + return writeVariable(path, value) +} + +// ================== 公共接口 ================== + +function connect(url?: string) { + if (connectionState.value === 'connected' || connectionState.value === 'connecting') + return + + if (url) { + wsUrl.value = url + connectionMode.value = 'websocket' + } + + if (connectionMode.value === 'websocket') { + connectWebSocket() + } + else { + connectMock() + } +} + +function disconnect() { + if (connectionMode.value === 'websocket') { + disconnectWebSocket() + } + else { + disconnectMock() + } + stopStaleCheck() + lastPushAt.value = 0 + isDataStale.value = false + connectionState.value = 'disconnected' +} + +function updateVariable(path: string, value: unknown) { + const list = [...mockVariables.value] + const index = list.findIndex(v => v.path === path) + if (index >= 0) { + list[index] = { ...list[index], value } + } + else { + list.push({ path, value, type: 'analog', quality: 'GOOD' }) + } + mockVariables.value = list +} + +function setVariables(variables: MockVariable[]) { + mockVariables.value = variables +} + +function onPush(callback: (variables: MockVariable[]) => void) { + onPushCallback = callback +} + +export function useDataConnection() { + return { + connectionState, + connectionMode, + mockVariables, + pushInterval, + wsUrl, + isDataStale, + lastPushAt, + connect, + disconnect, + updateVariable, + setVariables, + onPush, + writeVariable: writeVariableWithConfirm, + } +} diff --git a/packages/core/src/composables/useExportImport.ts b/packages/core/src/composables/useExportImport.ts new file mode 100644 index 0000000..c56b0d6 --- /dev/null +++ b/packages/core/src/composables/useExportImport.ts @@ -0,0 +1,293 @@ +import type { Layer } from '@cslab-dcs/schema' +import type { CanvasDetail } from '@/request/types/canvas.types' +import { ElMessage } from 'element-plus' +import { z } from 'zod' +import { createCanvasApi, getCanvasByIdApi, listCanvasesApi, updateComponentsLayer } from '@/api/canvas' + +// ── 导入数据校验 Schema ── +// 按实际存储格式(Layer 接口)校验,而非 canvasComponentSchema + +const layerImportSchema = z.object({ + id: z.string().min(1, '组件 ID 不能为空'), + type: z.string().min(1, '组件类型不能为空'), + x: z.number().finite(), + y: z.number().finite(), + width: z.number().positive(), + height: z.number().positive(), + config: z.record(z.unknown()).optional(), + style: z.record(z.unknown()).optional(), + tagNumber: z.string().optional(), + transition: z.string().optional(), + bindings: z.record(z.unknown()).optional(), + conditionalStyles: z.array(z.object({ + id: z.string(), + condition: z.string(), + style: z.record(z.unknown()), + priority: z.number().optional(), + enabled: z.boolean().optional(), + })).optional(), + events: z.array(z.object({ + id: z.string(), + trigger: z.string(), + condition: z.string().optional(), + action: z.object({ + type: z.string(), + payload: z.record(z.unknown()).optional(), + }), + })).optional(), +}).passthrough() + +const canvasImportSchema = z.object({ + version: z.literal(1), + type: z.literal('canvas'), + exportedAt: z.string(), + canvas: z.object({ + name: z.string().min(1, '画布名称不能为空').max(200, '画布名称过长'), + width: z.number().positive().max(10000, '画布宽度超出范围'), + height: z.number().positive().max(10000, '画布高度超出范围'), + backgroundColor: z.string().optional(), + backgroundImage: z.string().optional(), + }), + layers: z.array(layerImportSchema).max(5000, '组件数量超出限制'), +}) + +const projectImportSchema = z.object({ + version: z.literal(1), + type: z.literal('project'), + exportedAt: z.string(), + project: z.object({ + name: z.string().min(1).max(200), + }), + canvases: z.array(canvasImportSchema).max(100, '画布数量超出限制'), +}) + +// 导出数据格式 + +interface CanvasExportData { + version: 1 + type: 'canvas' + exportedAt: string + canvas: { + name: string + width: number + height: number + backgroundColor?: string + backgroundImage?: string + } + layers: unknown[] +} + +interface ProjectExportData { + version: 1 + type: 'project' + exportedAt: string + project: { + name: string + } + canvases: CanvasExportData[] +} + +// 工具函数 + +function downloadJSON(data: unknown, filename: string) { + const json = JSON.stringify(data, null, 2) + const blob = new Blob([json], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = filename + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) +} + +function formatDate() { + return new Date().toISOString().slice(0, 10) +} + +async function buildCanvasExport(canvasId: string): Promise { + const detail: CanvasDetail | null = await getCanvasByIdApi(canvasId) + if (!detail) + return null + return { + version: 1, + type: 'canvas', + exportedAt: new Date().toISOString(), + canvas: { + name: detail.name, + width: detail.width, + height: detail.height, + backgroundColor: detail.config?.style?.background?.color, + backgroundImage: detail.config?.style?.background?.src, + }, + layers: detail.components ?? [], + } +} + +function readFileAsText(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => resolve(reader.result as string) + reader.onerror = () => reject(reader.error) + reader.readAsText(file) + }) +} + +function pickFile(): Promise { + return new Promise((resolve) => { + const input = document.createElement('input') + input.type = 'file' + input.accept = '.json' + input.onchange = () => { + resolve(input.files?.[0] ?? null) + } + input.addEventListener('cancel', () => resolve(null)) + input.click() + }) +} + +// 公开 API + +export function useExportImport() { + async function exportCanvas(canvasId: string, canvasName?: string) { + try { + const data = await buildCanvasExport(canvasId) + if (!data) { + ElMessage.error('画布不存在') + return + } + downloadJSON(data, `${canvasName ?? data.canvas.name}_${formatDate()}.json`) + ElMessage.success('导出成功') + } + catch { + ElMessage.error('导出失败') + } + } + + async function exportProject(_projectId: string, projectName?: string) { + try { + const canvasList = await listCanvasesApi() + const canvases: CanvasExportData[] = [] + for (const c of canvasList) { + const data = await buildCanvasExport(c.id) + if (data) + canvases.push(data) + } + const exportData: ProjectExportData = { + version: 1, + type: 'project', + exportedAt: new Date().toISOString(), + project: { name: projectName ?? '未命名项目' }, + canvases, + } + downloadJSON(exportData, `${projectName ?? '项目'}_${formatDate()}.json`) + ElMessage.success(`导出成功,共 ${canvases.length} 个画布`) + } + catch { + ElMessage.error('项目导出失败') + } + } + + async function importCanvas(_projectId: string): Promise<{ success: boolean, canvasId?: string }> { + try { + const file = await pickFile() + if (!file) + return { success: false } + const text = await readFileAsText(file) + let raw: any + try { + raw = JSON.parse(text) + } + catch { + ElMessage.error('导入失败:JSON 格式错误') + return { success: false } + } + + // Schema 校验:验证结构完整性和组件合法性 + const parsed = canvasImportSchema.safeParse(raw) + if (!parsed.success) { + const firstError = parsed.error.errors[0]?.message ?? '数据格式不正确' + ElMessage.error(`导入失败:${firstError}`) + return { success: false } + } + + const data = parsed.data + const result = await createCanvasApi({ + name: `${data.canvas.name}(导入)`, + width: data.canvas.width, + height: data.canvas.height, + }) + + if (result && data.layers.length > 0) { + await updateComponentsLayer({ + id: result.id, + components: data.layers as Layer[], + }) + } + + ElMessage.success('画布导入成功') + return { success: true, canvasId: result?.id } + } + catch { + ElMessage.error('导入失败:画布创建出错') + return { success: false } + } + } + + async function importProject(): Promise<{ success: boolean }> { + try { + const file = await pickFile() + if (!file) + return { success: false } + const text = await readFileAsText(file) + let raw: any + try { + raw = JSON.parse(text) + } + catch { + ElMessage.error('导入失败:JSON 格式错误') + return { success: false } + } + + // Schema 校验:验证项目结构和所有画布的组件合法性 + const parsed = projectImportSchema.safeParse(raw) + if (!parsed.success) { + const firstError = parsed.error.errors[0]?.message ?? '数据格式不正确' + ElMessage.error(`导入失败:${firstError}`) + return { success: false } + } + + const data = parsed.data + if (data.canvases.length === 0) { + ElMessage.warning('项目中没有画布') + return { success: false } + } + + let importedCount = 0 + for (const canvasData of data.canvases) { + const result = await createCanvasApi({ + name: `${canvasData.canvas.name}(导入)`, + width: canvasData.canvas.width, + height: canvasData.canvas.height, + }) + if (result && canvasData.layers.length > 0) { + await updateComponentsLayer({ + id: result.id, + components: canvasData.layers as Layer[], + }) + } + importedCount++ + } + + ElMessage.success(`项目导入成功,共 ${importedCount} 个画布`) + return { success: true } + } + catch { + ElMessage.error('项目导入失败') + return { success: false } + } + } + + return { exportCanvas, exportProject, importCanvas, importProject } +} diff --git a/packages/core/src/composables/useOperationLog.ts b/packages/core/src/composables/useOperationLog.ts new file mode 100644 index 0000000..369ccd6 --- /dev/null +++ b/packages/core/src/composables/useOperationLog.ts @@ -0,0 +1,101 @@ +import { computed, ref } from 'vue' + +const RE_CSV_QUOTE = /"/g + +export interface OperationLogEntry { + id: string + timestamp: string + action: string + target: string + detail: string + result?: 'success' | 'failure' | 'cancelled' + operator?: string + mode?: 'web' | 'client' +} + +const MAX_ENTRIES = 500 + +// 模块级状态(单例共享) +const entries = ref([]) +const filterType = ref('') +const searchText = ref('') + +let nextId = Date.now() + +const filteredEntries = computed(() => { + let result = entries.value + if (filterType.value) { + result = result.filter(e => e.action === filterType.value) + } + if (searchText.value) { + const keyword = searchText.value.toLowerCase() + result = result.filter( + e => e.detail.toLowerCase().includes(keyword) + || e.target.toLowerCase().includes(keyword), + ) + } + return result +}) + +function log( + action: string, + target: string, + detail: string, + extra?: { result?: OperationLogEntry['result'], operator?: string, mode?: 'web' | 'client' }, +) { + const entry: OperationLogEntry = { + id: String(nextId++), + timestamp: new Date().toISOString(), + action, + target, + detail, + ...extra, + } + entries.value = [entry, ...entries.value].slice(0, MAX_ENTRIES) +} + +function clear() { + entries.value = [] +} + +function downloadBlob(blob: Blob, filename: string) { + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = filename + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) +} + +function exportJSON() { + const json = JSON.stringify(filteredEntries.value, null, 2) + const blob = new Blob([json], { type: 'application/json' }) + downloadBlob(blob, `操作日志_${new Date().toISOString().slice(0, 10)}.json`) +} + +function exportCSV() { + const header = 'ID,时间,操作,目标,详情,结果,操作员,模式' + const rows = filteredEntries.value.map(e => + [e.id, e.timestamp, e.action, e.target, e.detail, e.result || '', e.operator || '', e.mode || ''] + .map(v => `"${String(v).replace(RE_CSV_QUOTE, '""')}"`) + .join(','), + ) + const csv = [header, ...rows].join('\n') + const blob = new Blob([`\uFEFF${csv}`], { type: 'text/csv;charset=utf-8' }) + downloadBlob(blob, `操作日志_${new Date().toISOString().slice(0, 10)}.csv`) +} + +export function useOperationLog() { + return { + entries, + filteredEntries, + filterType, + searchText, + log, + clear, + exportJSON, + exportCSV, + } +} diff --git a/packages/core/src/composables/useRuntimeConsole.ts b/packages/core/src/composables/useRuntimeConsole.ts new file mode 100644 index 0000000..aae9669 --- /dev/null +++ b/packages/core/src/composables/useRuntimeConsole.ts @@ -0,0 +1,154 @@ +import { computed, ref } from 'vue' + +const RE_CSV_QUOTE = /"/g + +export type LogLevel = 'INFO' | 'WARN' | 'ERROR' +export type QualityStatus = 'GOOD' | 'BAD' | 'UNCERTAIN' + +export interface RuntimeLogEntry { + id: string + timestamp: string + level: LogLevel + category: string + message: string + quality?: QualityStatus + source?: string + variable?: string + value?: unknown + traceId?: string + mode?: 'web' | 'client' +} + +const MAX_ENTRIES = 1000 + +// 模块级状态(单例共享) +const entries = ref([]) +const levelFilter = ref('') +const categoryFilter = ref('') +const searchText = ref('') + +let nextId = Date.now() + +const filteredEntries = computed(() => { + let result = entries.value + if (levelFilter.value) { + result = result.filter(e => e.level === levelFilter.value) + } + if (categoryFilter.value) { + result = result.filter(e => e.category === categoryFilter.value) + } + if (searchText.value) { + const keyword = searchText.value.toLowerCase() + result = result.filter( + e => e.message.toLowerCase().includes(keyword) + || e.source?.toLowerCase().includes(keyword) + || e.category.toLowerCase().includes(keyword) + || e.variable?.toLowerCase().includes(keyword), + ) + } + return result +}) + +const categories = computed(() => { + const set = new Set() + for (const entry of entries.value) { + if (entry.category) + set.add(entry.category) + } + const arr = [...set] + arr.sort() + return arr +}) + +const levelCounts = computed(() => { + const counts = { INFO: 0, WARN: 0, ERROR: 0 } + for (const entry of entries.value) { + counts[entry.level]++ + } + return counts +}) + +function log( + level: LogLevel, + category: string, + message: string, + quality?: QualityStatus, + source?: string, + extra?: { variable?: string, value?: unknown, traceId?: string, mode?: 'web' | 'client' }, +) { + const entry: RuntimeLogEntry = { + id: String(nextId++), + timestamp: new Date().toISOString(), + level, + category, + message, + quality, + source, + ...extra, + } + entries.value = [entry, ...entries.value].slice(0, MAX_ENTRIES) +} + +function info(category: string, message: string, quality?: QualityStatus) { + log('INFO', category, message, quality) +} + +function warn(category: string, message: string, quality?: QualityStatus) { + log('WARN', category, message, quality) +} + +function error(category: string, message: string, source?: string) { + log('ERROR', category, message, undefined, source) +} + +function clear() { + entries.value = [] +} + +function downloadBlob(blob: Blob, filename: string) { + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = filename + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) +} + +function exportJSON() { + const json = JSON.stringify(filteredEntries.value, null, 2) + const blob = new Blob([json], { type: 'application/json' }) + downloadBlob(blob, `运行日志_${new Date().toISOString().slice(0, 10)}.json`) +} + +function exportCSV() { + const header = 'ID,时间,级别,分类,消息,质量位,来源,变量,值,跟踪ID,模式' + const rows = filteredEntries.value.map(e => + [e.id, e.timestamp, e.level, e.category, e.message, e.quality || '', e.source || '', e.variable || '', e.value ?? '', e.traceId || '', e.mode || ''] + .map(v => `"${String(v).replace(RE_CSV_QUOTE, '""')}"`) + .join(','), + ) + const csv = [header, ...rows].join('\n') + const blob = new Blob([`\uFEFF${csv}`], { type: 'text/csv;charset=utf-8' }) + downloadBlob(blob, `运行日志_${new Date().toISOString().slice(0, 10)}.csv`) +} + +export function useRuntimeConsole() { + return { + entries, + filteredEntries, + levelFilter, + categoryFilter, + searchText, + categories, + levelCounts, + log, + info, + warn, + error, + clear, + exportJSON, + exportCSV, + } +} diff --git a/packages/core/src/composables/useRuntimeSocket.ts b/packages/core/src/composables/useRuntimeSocket.ts new file mode 100644 index 0000000..b43ef05 --- /dev/null +++ b/packages/core/src/composables/useRuntimeSocket.ts @@ -0,0 +1,280 @@ +import { ref, shallowRef } from 'vue' + +// ── WebSocket 消息类型 ── + +/** 属性值结构:v=值, ut=单位类型, u=单位 */ +export interface ModulePropValue { + v: unknown + ut?: string + u?: string +} + +/** 单个模块位号下的 moduleProp */ +export type ModuleRuntimeData = Record + +/** WebSocket 推送的消息结构 */ +export interface RuntimeWsMessage { + flag: string + user_id?: number + version?: string + broadcast?: boolean + message_id?: string + data: { + info_type: 'moduleInfo' | 'progress' | string + task: string + info_data: Record + } | null +} + +/** 连接状态 */ +export type SocketStatus = 'disconnected' | 'connecting' | 'connected' + +const HEARTBEAT_INTERVAL = 10_000 +const HEARTBEAT_TIMEOUT = 3_000 +const RECONNECT_DELAY = 5_000 + +export function useRuntimeSocket() { + const status = ref('disconnected') + const taskId = ref('') + + // 最新一帧完整数据:key = 模块位号, value = 该模块属性数据 + const moduleDataMap = shallowRef>({}) + + // 当前计算时间 + const calculationTime = ref(0) + + // 进度信息 + const progressData = shallowRef(null) + + // 通用消息队列(非 moduleInfo / progress 的消息) + const messages = shallowRef([]) + + let socket: WebSocket | null = null + let heartbeatTimer: ReturnType | null = null + let pongTimer: ReturnType | null = null + let reconnectTimer: ReturnType | null = null + let isPong = false + let currentUserId: number | null = null + let shouldReconnect = true + + // RAF 批量合并:高频 WS 消息先写入缓冲区,每帧统一刷新一次 shallowRef + let pendingMerge: Record> = {} + let rafId: number | null = null + + function scheduleMerge() { + if (rafId !== null) + return + rafId = requestAnimationFrame(flushMerge) + } + + function flushMerge() { + rafId = null + const keys = Object.keys(pendingMerge) + if (!keys.length) + return + + const next = { ...moduleDataMap.value } + for (const key of keys) { + next[key] = { ...(next[key] || {}), ...pendingMerge[key] } + } + moduleDataMap.value = next + + pendingMerge = {} + } + + function cancelPendingMerge() { + if (rafId !== null) { + cancelAnimationFrame(rafId) + rafId = null + } + pendingMerge = {} + } + + // ── 心跳 ── + + function startHeartbeat() { + stopHeartbeat() + const check = () => { + if (socket?.readyState === WebSocket.OPEN) { + isPong = false + socket.send('ping') + pongTimer = setTimeout(() => { + if (!isPong) { + console.warn('[runtime-ws] 心跳超时,断开重连') + socket?.close() + } + }, HEARTBEAT_TIMEOUT) + } + heartbeatTimer = setTimeout(check, HEARTBEAT_INTERVAL) + } + check() + } + + function stopHeartbeat() { + if (heartbeatTimer) { + clearTimeout(heartbeatTimer) + heartbeatTimer = null + } + if (pongTimer) { + clearTimeout(pongTimer) + pongTimer = null + } + } + + // ── 消息处理 ── + + function handleMessage(event: MessageEvent) { + const raw = event.data as string + if (raw === 'pong') { + isPong = true + return + } + + if (!raw.startsWith('{')) + return + + try { + const msg = JSON.parse(raw) as RuntimeWsMessage + + // data 可能为 null 或非对象(如 [false, ""]),跳过 + if (!msg.data || typeof msg.data !== 'object' || Array.isArray(msg.data)) + return + + const { info_type, info_data } = msg.data + + if (info_type === 'moduleInfo') { + if (!info_data || !Object.keys(info_data).length) + return + + // 增量写入缓冲区,由 RAF 统一刷新到 moduleDataMap + for (const [key, value] of Object.entries(info_data)) { + if (key === 'calculationTime') { + calculationTime.value = value as number + } + else if (value && typeof value === 'object' && !Array.isArray(value)) { + const moduleObj = value as Record + const props = moduleObj.moduleProp + if (props && typeof props === 'object' && !Array.isArray(props)) { + if (!pendingMerge[key]) + pendingMerge[key] = {} + // 字符串类型的 v 需要 decodeURIComponent 解码 + for (const [pk, pv] of Object.entries(props as Record)) { + if (typeof pv.v === 'string') { + try { + pv.v = decodeURIComponent(pv.v) + } + catch {} + } + pendingMerge[key][pk] = pv + } + } + } + } + scheduleMerge() + } + else if (info_type === 'progress') { + progressData.value = msg + } + else { + messages.value = [...messages.value.slice(-499), msg] + } + } + catch (err) { + console.error('[runtime-ws] 解析消息失败', err) + } + } + + // ── 连接 ── + + function buildWsUrl(userId: number): string { + const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:' + return `${protocol}//${location.host}/chemical-chaos/v3/ws/client?user_id=${userId}&platform=web` + } + + function connect(userId: number) { + if (socket && socket.readyState < WebSocket.CLOSING) + return + + currentUserId = userId + shouldReconnect = true + status.value = 'connecting' + + const url = buildWsUrl(userId) + console.log('[runtime-ws] 正在连接', url) + socket = new WebSocket(url) + + socket.onopen = () => { + console.log('[runtime-ws] 连接成功') + status.value = 'connected' + startHeartbeat() + } + + socket.onmessage = handleMessage + + socket.onerror = (e) => { + console.error('[runtime-ws] 连接错误', e) + } + + socket.onclose = () => { + console.log('[runtime-ws] 连接关闭') + status.value = 'disconnected' + stopHeartbeat() + if (shouldReconnect) { + scheduleReconnect() + } + } + } + + function scheduleReconnect() { + if (reconnectTimer) + return + reconnectTimer = setTimeout(() => { + reconnectTimer = null + if (currentUserId && shouldReconnect) { + console.log('[runtime-ws] 尝试重连...') + connect(currentUserId) + } + }, RECONNECT_DELAY) + } + + function disconnect() { + shouldReconnect = false + if (reconnectTimer) { + clearTimeout(reconnectTimer) + reconnectTimer = null + } + stopHeartbeat() + cancelPendingMerge() + if (socket) { + socket.onclose = null + socket.close() + socket = null + } + status.value = 'disconnected' + } + + // ── 重置数据 ── + + function resetData() { + cancelPendingMerge() + moduleDataMap.value = {} + calculationTime.value = 0 + progressData.value = null + messages.value = [] + taskId.value = '' + } + + return { + // 状态 + status, + taskId, + moduleDataMap, + calculationTime, + progressData, + messages, + // 方法 + connect, + disconnect, + resetData, + } +} diff --git a/packages/core/src/config/index.ts b/packages/core/src/config/index.ts new file mode 100644 index 0000000..464e1c8 --- /dev/null +++ b/packages/core/src/config/index.ts @@ -0,0 +1,10 @@ +/** + * 统一的环境配置 + * 所有配置从 packages/core/.env* 文件中读取 + */ +export const config = { + /** API 接口基础地址 */ + apiBaseUrl: import.meta.env.VITE_API_BASE_URL as string, +} as const + +export type AppConfig = typeof config diff --git a/packages/core/src/constants/index.ts b/packages/core/src/constants/index.ts new file mode 100644 index 0000000..04e80e9 --- /dev/null +++ b/packages/core/src/constants/index.ts @@ -0,0 +1 @@ +export * from './modules/canvas' diff --git a/packages/core/src/constants/modules/canvas.ts b/packages/core/src/constants/modules/canvas.ts new file mode 100644 index 0000000..e7b37d8 --- /dev/null +++ b/packages/core/src/constants/modules/canvas.ts @@ -0,0 +1,8 @@ +// 新建画布默认尺寸 +export const DEFAULT_CANVAS_WIDTH = 1920 +export const DEFAULT_CANVAS_HEIGHT = 1080 +export const MIN_CANVAS_WIDTH = 360 +export const MIN_CANVAS_HEIGHT = 240 +export const MAX_CANVAS_SIZE = 20000 +export const GRID_SIZE = 10 +export const MAX_CANVAS_ZOOM_PERCENT = 300 diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..ca13cd1 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,33 @@ +import type { IPlatformBridge } from '@cslab-dcs/bridge' +import type { App } from 'vue' +import { createApp } from 'vue' +import AppRoot from './App.vue' +import router from './router' +import pinia from './stores' + +import 'uno.css' +import '@fortawesome/fontawesome-free/css/all.css' // Font Awesome +// Element Plus CSS 按需加载:模板组件由 ElementPlusResolver 自动引入样式 +// 以下为手动 import 的组件(ElMessage/ElMessageBox),需显式引入其样式 +import 'element-plus/es/components/message/style/css' +import 'element-plus/es/components/message-box/style/css' +import './assets/styles/index.scss' + +export function createDCSApp(bridge?: IPlatformBridge): App { + const app = createApp(AppRoot) + + app.use(pinia) + app.use(router) + + if (bridge) { + app.config.globalProperties.$bridge = bridge + app.provide('bridge', bridge) + } + + return app +} + +export { default as AppRoot } from './App.vue' +export * from './config' +export * from './router' +export * from './stores' diff --git a/packages/core/src/layout/index.tsx b/packages/core/src/layout/index.tsx new file mode 100644 index 0000000..67230b1 --- /dev/null +++ b/packages/core/src/layout/index.tsx @@ -0,0 +1,13 @@ +import { defineComponent } from 'vue' +import Provider from './provider.vue' +import RouteView from './route-view.vue' + +export default defineComponent({ + setup() { + return () => ( + + + + ) + }, +}) diff --git a/packages/core/src/layout/provider.vue b/packages/core/src/layout/provider.vue new file mode 100644 index 0000000..204cb3c --- /dev/null +++ b/packages/core/src/layout/provider.vue @@ -0,0 +1,10 @@ + + + diff --git a/packages/core/src/layout/route-view.vue b/packages/core/src/layout/route-view.vue new file mode 100644 index 0000000..8c24db3 --- /dev/null +++ b/packages/core/src/layout/route-view.vue @@ -0,0 +1,13 @@ + + + diff --git a/packages/core/src/request/adapters/http/canvas-repository.ts b/packages/core/src/request/adapters/http/canvas-repository.ts new file mode 100644 index 0000000..c556a37 --- /dev/null +++ b/packages/core/src/request/adapters/http/canvas-repository.ts @@ -0,0 +1,103 @@ +import type { $Fetch } from 'ofetch' +import type { CanvasRepository } from '../../types/canvas.repository' +import type { + CanvasDetail, + CanvasSummary, + CreateCanvasInput, + DeleteCanvasInput, + DuplicateCanvasInput, + ReorderCanvasesInput, + SaveCanvasComponentsInput, + UpdateCanvasBaseLayerInput, + UpdateCanvasInput, +} from '../../types/canvas.types' + +interface HttpCanvasRepositoryOptions { + fetcher: $Fetch +} + +// 画布 HTTP 仓库:与后端 REST 接口一一映射 +export class HttpCanvasRepository implements CanvasRepository { + private readonly fetcher: $Fetch + + constructor(options: HttpCanvasRepositoryOptions) { + this.fetcher = options.fetcher + } + + async list(projectId: string): Promise { + return this.fetcher('/canvases', { + method: 'GET', + query: { projectId }, + }) + } + + async getById(canvasId: string): Promise { + return this.fetcher(`/canvases/${canvasId}`, { + method: 'GET', + }) + } + + async create(input: CreateCanvasInput): Promise { + return this.fetcher('/canvases', { + method: 'POST', + body: input, + }) + } + + async update(input: UpdateCanvasInput): Promise { + const { canvasId, ...rest } = input + return this.fetcher(`/canvases/${canvasId}/update`, { + method: 'PUT', + body: rest, + }) + } + + async duplicate(input: DuplicateCanvasInput): Promise { + return this.fetcher(`/canvases/${input.canvasId}/duplicate`, { + method: 'POST', + body: { + targetProjectId: input.targetProjectId, + name: input.name, + }, + }) + } + + async remove(input: DeleteCanvasInput): Promise { + await this.fetcher(`/canvases/${input.canvasId}`, { + method: 'DELETE', + }) + } + + // 全量覆盖组件快照,保持前后端一致 + async updateComponentsLayer(input: SaveCanvasComponentsInput): Promise { + await this.fetcher(`/canvases/${input.canvasId}/components`, { + method: 'PUT', + body: { + components: input.components, + }, + }) + } + + async reorder(input: ReorderCanvasesInput): Promise { + await this.fetcher(`/canvases/reorder`, { + method: 'PUT', + body: { + projectId: input.projectId, + canvasIds: input.canvasIds, + }, + }) + } + + async updateBaseLayer(input: UpdateCanvasBaseLayerInput): Promise { + await this.fetcher(`/canvases/${input.canvasId}/base-layer`, { + method: 'PUT', + body: { + width: input.width, + height: input.height, + thumbnail: input.thumbnail, + lockAspectRatio: input.lockAspectRatio, + backgroundColor: input.backgroundColor, + }, + }) + } +} diff --git a/packages/core/src/request/adapters/http/project-repository.ts b/packages/core/src/request/adapters/http/project-repository.ts new file mode 100644 index 0000000..aef4726 --- /dev/null +++ b/packages/core/src/request/adapters/http/project-repository.ts @@ -0,0 +1,29 @@ +import type { $Fetch } from 'ofetch' +import type { ProjectRepository } from '../../types/project.repository' +import type { ProjectSummary } from '../../types/project.types' + +interface HttpProjectRepositoryOptions { + fetcher: $Fetch +} + +// 项目 HTTP 仓库 +export class HttpProjectRepository implements ProjectRepository { + private readonly fetcher: $Fetch + + constructor(options: HttpProjectRepositoryOptions) { + this.fetcher = options.fetcher + } + + async list(): Promise { + return this.fetcher('/project/', { + method: 'GET', + query: { isPublic: 0, guide_code: '017' }, + }) + } + + async getById(projectId: string): Promise { + const list = await this.list() + + return list.find(project => project.project_pk === projectId) || null + } +} diff --git a/packages/core/src/request/adapters/indexeddb/canvas-repository.ts b/packages/core/src/request/adapters/indexeddb/canvas-repository.ts new file mode 100644 index 0000000..cdc86ae --- /dev/null +++ b/packages/core/src/request/adapters/indexeddb/canvas-repository.ts @@ -0,0 +1,548 @@ +import type { Background, CanvasConfig } from '@cslab-dcs/schema' +import type { DCSCanvasRecord, DCSComponentRecord, DCSProjectRecord } from '../../types' +import type { CanvasRepository } from '../../types/canvas.repository' +import type { + CanvasDetail, + CanvasSummary, + CreateCanvasInput, + DeleteCanvasInput, + DuplicateCanvasInput, + ReorderCanvasesInput, + SaveCanvasComponentsInput, + UpdateCanvasBaseLayerInput, + UpdateCanvasInput, +} from '../../types/canvas.types' +import { createEmptyCanvas } from '@cslab-dcs/schema' +import { DEFAULT_CANVAS_HEIGHT, DEFAULT_CANVAS_WIDTH, MIN_CANVAS_HEIGHT, MIN_CANVAS_WIDTH } from '@/constants' +import { cloneData, generateUUID } from '@/utils' +import { + DCS_STORE_NAMES, + DEFAULT_LOCAL_PROJECT_NAME, +} from '../../constants' +import { deleteByIndex, getAllByIndex, openDcsDatabase, requestToPromise, transactionToPromise } from './utils/db' + +// 统一当前时间,保证写入字段一致 +function nowISO(): string { + return new Date().toISOString() +} + +// 画布名称基础校验 +function normalizeCanvasName(name: string) { + const nextName = name.trim() + if (!nextName) { + throw new Error('画布名称不能为空') + } + return nextName +} + +// 数据库存储结构 -> 列表展示结构 +function summarizeCanvas(record: DCSCanvasRecord): CanvasSummary { + return { + id: record.id, + projectId: record.projectId, + name: record.name, + description: record.description || '', + thumbnail: record.thumbnail || '', + updatedAt: record.updatedAt, + createdAt: record.createdAt, + order: record.order, + } +} + +// 组件排序规则:优先按显式 order,其次按创建时间,最后按主键。 +function sortComponentsByOrder(records: DCSComponentRecord[]) { + return records.toSorted((left, right) => { + const leftOrder = typeof left.order === 'number' ? left.order : Number.MAX_SAFE_INTEGER + const rightOrder = typeof right.order === 'number' ? right.order : Number.MAX_SAFE_INTEGER + if (leftOrder !== rightOrder) { + return leftOrder - rightOrder + } + + const createdAtCompare = left.createdAt.localeCompare(right.createdAt) + if (createdAtCompare !== 0) { + return createdAtCompare + } + + return left.id.localeCompare(right.id) + }) +} + +// 组装画布详情(含组件) +function toCanvasDetail(record: DCSCanvasRecord, componentRecords: DCSComponentRecord[]): CanvasDetail { + const components = sortComponentsByOrder(componentRecords).map((item) => { + const component = item.component + // progress → bar 兼容映射 + if ((component.type as string) === 'progress') { + (component as any).type = 'bar' + } + return component + }) + + return { + ...summarizeCanvas(record), + width: record.width, + height: record.height, + type: record.type, + version: record.version, + config: record.config, + vars: record.vars, + components, + } +} + +// 列表排序规则:先按 order,再按更新时间 +function sortCanvasesByOrder(records: DCSCanvasRecord[]) { + return records.toSorted((a, b) => { + if (a.order !== b.order) { + return a.order - b.order + } + return b.updatedAt.localeCompare(a.updatedAt) + }) +} + +// 复制命名策略:副本 / 副本 (2) / 副本 (3) +function createDuplicateName(baseName: string, existingNames: Set) { + const defaultName = `${baseName} 副本` + if (!existingNames.has(defaultName)) { + return defaultName + } + + let counter = 2 + let candidate = `${defaultName} (${counter})` + while (existingNames.has(candidate)) { + counter += 1 + candidate = `${defaultName} (${counter})` + } + + return candidate +} + +// 为首次出现的 projectId 生成默认项目名 +function createProjectName(projectId: string) { + if (!projectId || projectId === 'local-default-project') { + return DEFAULT_LOCAL_PROJECT_NAME + } + return `项目-${projectId.slice(0, 8)}` +} + +// 若项目不存在则自动创建(离线场景容错) +async function ensureProjectRecord( + projectStore: IDBObjectStore, + projectId: string, + now: string, +): Promise { + const existing = await requestToPromise(projectStore.get(projectId)) as DCSProjectRecord | undefined + if (existing) { + return existing + } + + const createdProject: DCSProjectRecord = { + id: projectId, + name: createProjectName(projectId), + base_pro: 'basic', + code: '001', + createdAt: now, + updatedAt: now, + } + + await requestToPromise(projectStore.put(createdProject)) + return createdProject +} + +// 更新项目 updatedAt,用于最近更新时间排序 +async function touchProject(projectStore: IDBObjectStore, projectId: string, now: string) { + const project = await ensureProjectRecord(projectStore, projectId, now) + if (project.updatedAt === now) { + return + } + + await requestToPromise(projectStore.put({ + ...project, + updatedAt: now, + })) +} + +// IndexedDB 画布仓库实现 +export class IndexedDBCanvasRepository implements CanvasRepository { + // 查询项目下画布列表 + async list(projectId: string): Promise { + const db = await openDcsDatabase() + const transaction = db.transaction([DCS_STORE_NAMES.canvases], 'readonly') + const canvasStore = transaction.objectStore(DCS_STORE_NAMES.canvases) + + const records = await getAllByIndex(canvasStore, 'projectId', projectId) + + await transactionToPromise(transaction) + return sortCanvasesByOrder(records).map(summarizeCanvas) + } + + // 查询画布详情(画布 + 组件) + async getById(canvasId: string): Promise { + const db = await openDcsDatabase() + const transaction = db.transaction([DCS_STORE_NAMES.canvases, DCS_STORE_NAMES.components], 'readonly') + const canvasStore = transaction.objectStore(DCS_STORE_NAMES.canvases) + const componentStore = transaction.objectStore(DCS_STORE_NAMES.components) + + const canvas = await requestToPromise(canvasStore.get(canvasId)) as DCSCanvasRecord | undefined + if (!canvas) { + await transactionToPromise(transaction) + return null + } + + const components = await getAllByIndex(componentStore, 'canvasId', canvasId) + + await transactionToPromise(transaction) + return toCanvasDetail(canvas, components) + } + + // 新建画布 + async create(input: CreateCanvasInput): Promise { + const name = normalizeCanvasName(input.name) + const now = nowISO() + const db = await openDcsDatabase() + + const transaction = db.transaction([DCS_STORE_NAMES.projects, DCS_STORE_NAMES.canvases], 'readwrite') + const projectStore = transaction.objectStore(DCS_STORE_NAMES.projects) + const canvasStore = transaction.objectStore(DCS_STORE_NAMES.canvases) + + await ensureProjectRecord(projectStore, input.projectId, now) + + const existingCanvases = await getAllByIndex(canvasStore, 'projectId', input.projectId) + const nextOrder = existingCanvases.reduce((maxOrder, canvas) => Math.max(maxOrder, canvas.order), -1) + 1 + const requestedWidth = Number(input.width) + const requestedHeight = Number(input.height) + const safeWidth = Number.isFinite(requestedWidth) && requestedWidth > 0 + ? Math.max(MIN_CANVAS_WIDTH, Math.round(requestedWidth)) + : DEFAULT_CANVAS_WIDTH + const safeHeight = Number.isFinite(requestedHeight) && requestedHeight > 0 + ? Math.max(MIN_CANVAS_HEIGHT, Math.round(requestedHeight)) + : DEFAULT_CANVAS_HEIGHT + + const emptyCanvas = createEmptyCanvas({ + id: generateUUID(), + name, + width: safeWidth, + height: safeHeight, + }) + + const record: DCSCanvasRecord = { + id: emptyCanvas.id, + projectId: input.projectId, + name, + description: input.description || '', + thumbnail: input.thumbnail || '', + width: emptyCanvas.width, + height: emptyCanvas.height, + type: emptyCanvas.type, + version: emptyCanvas.version, + config: input.config || emptyCanvas.config, + vars: input.vars || emptyCanvas.vars, + order: nextOrder, + createdAt: now, + updatedAt: now, + } + + await requestToPromise(canvasStore.put(record)) + await touchProject(projectStore, input.projectId, now) + + await transactionToPromise(transaction) + return summarizeCanvas(record) + } + + // 重命名画布 + async update(input: UpdateCanvasInput): Promise { + const { name: _name, canvasId, ...rest } = input + + const name = normalizeCanvasName(_name) + const now = nowISO() + const db = await openDcsDatabase() + + const transaction = db.transaction([DCS_STORE_NAMES.projects, DCS_STORE_NAMES.canvases], 'readwrite') + const canvasStore = transaction.objectStore(DCS_STORE_NAMES.canvases) + const projectStore = transaction.objectStore(DCS_STORE_NAMES.projects) + + const canvasRecord = await requestToPromise(canvasStore.get(canvasId)) as DCSCanvasRecord | undefined + if (!canvasRecord) { + throw new Error('画布不存在') + } + + const nextCanvasRecord: DCSCanvasRecord = { + ...canvasRecord, + ...rest, + name, + updatedAt: now, + } + + await requestToPromise(canvasStore.put(nextCanvasRecord)) + await touchProject(projectStore, nextCanvasRecord.projectId, now) + + await transactionToPromise(transaction) + return summarizeCanvas(nextCanvasRecord) + } + + // 复制画布与其组件快照 + async duplicate(input: DuplicateCanvasInput): Promise { + const now = nowISO() + const db = await openDcsDatabase() + + const transaction = db.transaction([ + DCS_STORE_NAMES.projects, + DCS_STORE_NAMES.canvases, + DCS_STORE_NAMES.components, + ], 'readwrite') + + const projectStore = transaction.objectStore(DCS_STORE_NAMES.projects) + const canvasStore = transaction.objectStore(DCS_STORE_NAMES.canvases) + const componentStore = transaction.objectStore(DCS_STORE_NAMES.components) + + const sourceCanvas = await requestToPromise(canvasStore.get(input.canvasId)) as DCSCanvasRecord | undefined + if (!sourceCanvas) { + throw new Error('要复制的画布不存在') + } + + const targetProjectId = input.targetProjectId || sourceCanvas.projectId + await ensureProjectRecord(projectStore, targetProjectId, now) + + const targetProjectCanvases = await getAllByIndex(canvasStore, 'projectId', targetProjectId) + const existingNames = new Set(targetProjectCanvases.map(item => item.name)) + const duplicateName = input.name + ? normalizeCanvasName(input.name) + : createDuplicateName(sourceCanvas.name, existingNames) + + const nextOrder = targetProjectCanvases.reduce((maxOrder, canvas) => Math.max(maxOrder, canvas.order), -1) + 1 + const duplicatedCanvasId = generateUUID() + + const duplicatedCanvas: DCSCanvasRecord = { + ...cloneData(sourceCanvas), + id: duplicatedCanvasId, + projectId: targetProjectId, + name: duplicateName, + order: nextOrder, + createdAt: now, + updatedAt: now, + } + + await requestToPromise(canvasStore.put(duplicatedCanvas)) + + const sourceComponents = sortComponentsByOrder( + await getAllByIndex(componentStore, 'canvasId', sourceCanvas.id), + ) + for (const [index, sourceComponentRecord] of sourceComponents.entries()) { + const component = cloneData(sourceComponentRecord.component) + const nextComponentId = generateUUID() + component.id = nextComponentId + + const duplicatedComponentRecord: DCSComponentRecord = { + ...cloneData(sourceComponentRecord), + id: nextComponentId, + projectId: targetProjectId, + canvasId: duplicatedCanvasId, + component, + order: typeof sourceComponentRecord.order === 'number' ? sourceComponentRecord.order : index, + createdAt: now, + updatedAt: now, + } + + await requestToPromise(componentStore.put(duplicatedComponentRecord)) + } + + await touchProject(projectStore, sourceCanvas.projectId, now) + if (targetProjectId !== sourceCanvas.projectId) { + await touchProject(projectStore, targetProjectId, now) + } + + await transactionToPromise(transaction) + return summarizeCanvas(duplicatedCanvas) + } + + // 删除画布并级联删除组件、日志,同时重排画布顺序 + async remove(input: DeleteCanvasInput): Promise { + const now = nowISO() + const db = await openDcsDatabase() + + const transaction = db.transaction([ + DCS_STORE_NAMES.projects, + DCS_STORE_NAMES.canvases, + DCS_STORE_NAMES.components, + DCS_STORE_NAMES.logs, + ], 'readwrite') + + const projectStore = transaction.objectStore(DCS_STORE_NAMES.projects) + const canvasStore = transaction.objectStore(DCS_STORE_NAMES.canvases) + const componentStore = transaction.objectStore(DCS_STORE_NAMES.components) + const logStore = transaction.objectStore(DCS_STORE_NAMES.logs) + + const canvas = await requestToPromise(canvasStore.get(input.canvasId)) as DCSCanvasRecord | undefined + if (!canvas) { + await transactionToPromise(transaction) + return + } + + await requestToPromise(canvasStore.delete(input.canvasId)) + await deleteByIndex(componentStore, 'canvasId', input.canvasId) + await deleteByIndex(logStore, 'canvasId', input.canvasId) + + const remainingCanvases = sortCanvasesByOrder( + await getAllByIndex(canvasStore, 'projectId', canvas.projectId), + ) + + for (let index = 0; index < remainingCanvases.length; index += 1) { + const record = remainingCanvases[index] + if (record.order === index) { + continue + } + await requestToPromise(canvasStore.put({ + ...record, + order: index, + updatedAt: now, + })) + } + + await touchProject(projectStore, canvas.projectId, now) + await transactionToPromise(transaction) + } + + // 覆盖保存组件快照(编辑器自动保存入口) + async updateComponentsLayer(input: SaveCanvasComponentsInput): Promise { + const now = nowISO() + const db = await openDcsDatabase() + + const transaction = db.transaction([ + DCS_STORE_NAMES.projects, + DCS_STORE_NAMES.canvases, + DCS_STORE_NAMES.components, + ], 'readwrite') + + const projectStore = transaction.objectStore(DCS_STORE_NAMES.projects) + const canvasStore = transaction.objectStore(DCS_STORE_NAMES.canvases) + const componentStore = transaction.objectStore(DCS_STORE_NAMES.components) + + const canvas = await requestToPromise(canvasStore.get(input.canvasId)) as DCSCanvasRecord | undefined + if (!canvas) { + throw new Error('画布不存在') + } + + await deleteByIndex(componentStore, 'canvasId', input.canvasId) + + for (const [order, rawComponent] of input.components.entries()) { + const component = cloneData(rawComponent) + const componentId = typeof component.id === 'string' && component.id + ? component.id + : generateUUID() + + component.id = componentId + + const componentRecord: DCSComponentRecord = { + id: componentId, + projectId: canvas.projectId, + canvasId: input.canvasId, + component, + order, + createdAt: now, + updatedAt: now, + } + + await requestToPromise(componentStore.put(componentRecord)) + } + + await requestToPromise(canvasStore.put({ + ...canvas, + updatedAt: now, + })) + + await touchProject(projectStore, canvas.projectId, now) + await transactionToPromise(transaction) + } + + // 批量更新画布排序 + async reorder(input: ReorderCanvasesInput): Promise { + const now = nowISO() + const db = await openDcsDatabase() + + const transaction = db.transaction([DCS_STORE_NAMES.canvases], 'readwrite') + const canvasStore = transaction.objectStore(DCS_STORE_NAMES.canvases) + + for (let index = 0; index < input.canvasIds.length; index += 1) { + const canvasId = input.canvasIds[index] + const record = await requestToPromise(canvasStore.get(canvasId)) as DCSCanvasRecord | undefined + if (!record || record.order === index) { + continue + } + await requestToPromise(canvasStore.put({ + ...record, + order: index, + updatedAt: now, + })) + } + + await transactionToPromise(transaction) + } + + // 更新背景图层(底图)尺寸与锁定宽高比配置 + async updateBaseLayer(input: UpdateCanvasBaseLayerInput): Promise { + const now = nowISO() + const db = await openDcsDatabase() + + const transaction = db.transaction([DCS_STORE_NAMES.projects, DCS_STORE_NAMES.canvases], 'readwrite') + const projectStore = transaction.objectStore(DCS_STORE_NAMES.projects) + const canvasStore = transaction.objectStore(DCS_STORE_NAMES.canvases) + + const canvas = await requestToPromise(canvasStore.get(input.canvasId)) as DCSCanvasRecord | undefined + if (!canvas) { + throw new Error('画布不存在') + } + + const safeWidth = Math.max(MIN_CANVAS_WIDTH, Math.round(input.width)) + const safeHeight = Math.max(MIN_CANVAS_HEIGHT, Math.round(input.height)) + + const nextConfig: CanvasConfig = cloneData(canvas.config || {}) + const nextStyle = cloneData(nextConfig.style || {}) + const nextBackground: Background = { + type: 'color', + ...(cloneData(nextStyle.background || {}) as Partial), + } + nextBackground.width = safeWidth + nextBackground.height = safeHeight + if (typeof input.lockAspectRatio === 'boolean') { + nextBackground.lockAspectRatio = input.lockAspectRatio + } + if (typeof input.backgroundColor === 'string' && input.backgroundColor.trim()) { + nextBackground.color = input.backgroundColor.trim() + } + + let nextThumbnail = canvas.thumbnail + if (typeof input.thumbnail === 'string' && input.thumbnail.trim()) { + nextThumbnail = input.thumbnail + nextBackground.type = 'image' + nextBackground.src = input.thumbnail + } + else if (input.thumbnail === null) { + nextThumbnail = '' + nextBackground.type = 'color' + delete nextBackground.src + } + else if (canvas.thumbnail) { + nextBackground.type = 'image' + nextBackground.src = canvas.thumbnail + } + + const nextCanvas: DCSCanvasRecord = { + ...canvas, + width: safeWidth, + height: safeHeight, + thumbnail: nextThumbnail, + config: { + ...nextConfig, + style: { + ...nextStyle, + background: nextBackground, + }, + }, + updatedAt: now, + } + + await requestToPromise(canvasStore.put(nextCanvas)) + await touchProject(projectStore, canvas.projectId, now) + await transactionToPromise(transaction) + } +} diff --git a/packages/core/src/request/adapters/indexeddb/project-repository.ts b/packages/core/src/request/adapters/indexeddb/project-repository.ts new file mode 100644 index 0000000..c804e10 --- /dev/null +++ b/packages/core/src/request/adapters/indexeddb/project-repository.ts @@ -0,0 +1,133 @@ +import type { DCSCanvasRecord, DCSProjectRecord } from '../../types' +import type { ProjectRepository } from '../../types/project.repository' +import type { CreateProjectInput, DeleteProjectInput, ProjectSummary, UpdateProjectInput } from '../../types/project.types' +import { generateUUID } from '@/utils' +import { DCS_STORE_NAMES } from '../../constants' +import { deleteByIndex, getAllByIndex, openDcsDatabase, requestToPromise, transactionToPromise } from './utils/db' + +// 统一时间来源 +function nowISO() { + return new Date().toISOString() +} + +// 项目名基础校验 +function normalizeProjectName(name: string) { + const nextName = name.trim() + if (!nextName) { + throw new Error('项目名称不能为空') + } + return nextName +} + +// 数据库存储结构 -> 对外结构 +function summarizeProject(project: DCSProjectRecord): ProjectSummary { + return { + project_pk: project.id, + name: project.name, + describe: project.describe || '', + code: project.code, + is_public: '0', + base_pro: project.base_pro, + create_timestamp: project.createdAt, + } +} + +// IndexedDB 项目仓库实现 +export class IndexedDBProjectRepository implements ProjectRepository { + // 按最近更新时间倒序 + async list(): Promise { + const db = await openDcsDatabase() + const transaction = db.transaction([DCS_STORE_NAMES.projects], 'readonly') + const projectStore = transaction.objectStore(DCS_STORE_NAMES.projects) + + const projects = await requestToPromise(projectStore.getAll()) as DCSProjectRecord[] + await transactionToPromise(transaction) + + return projects.toSorted((a, b) => b.updatedAt.localeCompare(a.updatedAt)) + .map(summarizeProject) + } + + // 查询项目详情 + async getById(projectId: string): Promise { + const db = await openDcsDatabase() + const transaction = db.transaction([DCS_STORE_NAMES.projects], 'readonly') + const projectStore = transaction.objectStore(DCS_STORE_NAMES.projects) + + const project = await requestToPromise(projectStore.get(projectId)) as DCSProjectRecord | undefined + await transactionToPromise(transaction) + + return project ? summarizeProject(project) : null + } + + // 新建项目 + async create(input: CreateProjectInput): Promise { + const now = nowISO() + const project: DCSProjectRecord = { + id: generateUUID(), + name: normalizeProjectName(input.name), + describe: input.describe, + code: input.code, + base_pro: input.assocPro, + createdAt: now, + updatedAt: now, + } + + const db = await openDcsDatabase() + const transaction = db.transaction([DCS_STORE_NAMES.projects], 'readwrite') + const projectStore = transaction.objectStore(DCS_STORE_NAMES.projects) + + await requestToPromise(projectStore.put(project)) + await transactionToPromise(transaction) + return summarizeProject(project) + } + + // 重命名项目 + async update(input: UpdateProjectInput): Promise { + const now = nowISO() + const db = await openDcsDatabase() + const transaction = db.transaction([DCS_STORE_NAMES.projects], 'readwrite') + const projectStore = transaction.objectStore(DCS_STORE_NAMES.projects) + + const project = await requestToPromise(projectStore.get(input.pk)) as DCSProjectRecord | undefined + if (!project) { + throw new Error('项目不存在') + } + + const nextProject: DCSProjectRecord = { + ...project, + name: normalizeProjectName(input.parameter.name), + describe: input.parameter.describe, + updatedAt: now, + } + + await requestToPromise(projectStore.put(nextProject)) + await transactionToPromise(transaction) + return summarizeProject(nextProject) + } + + // 删除项目并级联清理画布、组件、日志 + async remove(input: DeleteProjectInput): Promise { + const db = await openDcsDatabase() + const transaction = db.transaction([ + DCS_STORE_NAMES.projects, + DCS_STORE_NAMES.canvases, + DCS_STORE_NAMES.components, + DCS_STORE_NAMES.logs, + ], 'readwrite') + + const projectStore = transaction.objectStore(DCS_STORE_NAMES.projects) + const canvasStore = transaction.objectStore(DCS_STORE_NAMES.canvases) + const componentStore = transaction.objectStore(DCS_STORE_NAMES.components) + const logStore = transaction.objectStore(DCS_STORE_NAMES.logs) + + const canvases = await getAllByIndex(canvasStore, 'projectId', input.pk) + for (const canvas of canvases) { + await requestToPromise(canvasStore.delete(canvas.id)) + } + + await deleteByIndex(componentStore, 'projectId', input.pk) + await deleteByIndex(logStore, 'projectId', input.pk) + await requestToPromise(projectStore.delete(input.pk)) + await transactionToPromise(transaction) + } +} diff --git a/packages/core/src/request/adapters/indexeddb/utils/db.ts b/packages/core/src/request/adapters/indexeddb/utils/db.ts new file mode 100644 index 0000000..0022527 --- /dev/null +++ b/packages/core/src/request/adapters/indexeddb/utils/db.ts @@ -0,0 +1,181 @@ +import { DCS_DB_NAME, DCS_DB_VERSION, DCS_STORE_NAMES } from '../../../constants' +import { migrateCanvasRecordV1ToV2, migrateComponentRecordV1ToV2 } from './migrations' + +// 单例数据库连接,避免重复 open +let databasePromise: Promise | null = null + +function assertIndexedDB() { + if (typeof indexedDB === 'undefined') { + throw new TypeError('IndexedDB is not available in the current environment') + } +} + +// 初始化所有 store 与索引(可重复执行) +function createStoresIfNeeded(db: IDBDatabase) { + if (!db.objectStoreNames.contains(DCS_STORE_NAMES.projects)) { + const store = db.createObjectStore(DCS_STORE_NAMES.projects, { keyPath: 'id' }) + store.createIndex('name', 'name', { unique: false }) + store.createIndex('updatedAt', 'updatedAt', { unique: false }) + } + + if (!db.objectStoreNames.contains(DCS_STORE_NAMES.canvases)) { + const store = db.createObjectStore(DCS_STORE_NAMES.canvases, { keyPath: 'id' }) + store.createIndex('projectId', 'projectId', { unique: false }) + store.createIndex('projectIdAndOrder', ['projectId', 'order'], { unique: false }) + store.createIndex('updatedAt', 'updatedAt', { unique: false }) + } + + if (!db.objectStoreNames.contains(DCS_STORE_NAMES.components)) { + const store = db.createObjectStore(DCS_STORE_NAMES.components, { keyPath: 'id' }) + store.createIndex('projectId', 'projectId', { unique: false }) + store.createIndex('canvasId', 'canvasId', { unique: false }) + store.createIndex('projectIdAndCanvasId', ['projectId', 'canvasId'], { unique: false }) + } + + if (!db.objectStoreNames.contains(DCS_STORE_NAMES.logs)) { + const store = db.createObjectStore(DCS_STORE_NAMES.logs, { keyPath: 'id' }) + store.createIndex('projectId', 'projectId', { unique: false }) + store.createIndex('canvasId', 'canvasId', { unique: false }) + store.createIndex('createdAt', 'createdAt', { unique: false }) + store.createIndex('mode', 'payload.mode', { unique: false }) + store.createIndex('level', 'payload.level', { unique: false }) + } + + if (!db.objectStoreNames.contains(DCS_STORE_NAMES.meta)) { + db.createObjectStore(DCS_STORE_NAMES.meta, { keyPath: 'key' }) + } +} + +// v1 -> v2 数据迁移: +// 画布字段收敛到 config,组件 configs 改名为 config +function migrateV1ToV2(transaction: IDBTransaction) { + if (transaction.objectStoreNames.contains(DCS_STORE_NAMES.canvases)) { + const canvases = transaction.objectStore(DCS_STORE_NAMES.canvases) + canvases.openCursor().onsuccess = (event) => { + const cursor = (event.target as IDBRequest).result + if (!cursor) { + return + } + const migratedValue = migrateCanvasRecordV1ToV2(cursor.value) + cursor.update(migratedValue) + cursor.continue() + } + } + + if (transaction.objectStoreNames.contains(DCS_STORE_NAMES.components)) { + const components = transaction.objectStore(DCS_STORE_NAMES.components) + components.openCursor().onsuccess = (event) => { + const cursor = (event.target as IDBRequest).result + if (!cursor) { + return + } + const migratedValue = migrateComponentRecordV1ToV2(cursor.value) + cursor.update(migratedValue) + cursor.continue() + } + } +} + +// 升级入口:先建表,再按旧版本号执行迁移 +function handleUpgrade(event: IDBVersionChangeEvent) { + const request = event.target as IDBOpenDBRequest + const db = request.result + const transaction = request.transaction + + createStoresIfNeeded(db) + + if (!transaction) { + return + } + + if (event.oldVersion < 2 && event.oldVersion >= 1) { + migrateV1ToV2(transaction) + } +} + +// 将 IDBRequest 包装为 Promise,便于 async/await +export function requestToPromise(request: IDBRequest) { + return new Promise((resolve, reject) => { + request.onsuccess = () => resolve(request.result) + request.onerror = () => reject(request.error ?? new Error('IndexedDB request failed')) + }) +} + +// 将 IDBTransaction 包装为 Promise +export function transactionToPromise(transaction: IDBTransaction) { + return new Promise((resolve, reject) => { + transaction.oncomplete = () => resolve() + transaction.onerror = () => reject(transaction.error ?? new Error('IndexedDB transaction failed')) + transaction.onabort = () => reject(transaction.error ?? new Error('IndexedDB transaction aborted')) + }) +} + +// 打开数据库并返回连接(含升级与阻塞处理) +export async function openDcsDatabase() { + assertIndexedDB() + + if (databasePromise) { + return databasePromise + } + + databasePromise = new Promise((resolve, reject) => { + const request = indexedDB.open(DCS_DB_NAME, DCS_DB_VERSION) + + request.onupgradeneeded = handleUpgrade + request.onerror = () => reject(request.error ?? new Error('Failed to open IndexedDB')) + request.onblocked = () => reject(new Error('IndexedDB upgrade is blocked by another tab')) + + request.onsuccess = () => { + const db = request.result + db.onversionchange = () => { + db.close() + } + resolve(db) + } + }) + + return databasePromise +} + +// 在测试或异常恢复场景中重置连接缓存 +export function resetDcsDatabaseConnection() { + databasePromise = null +} + +// 按索引值批量读取 +export async function getAllByIndex( + store: IDBObjectStore, + indexName: string, + key: IDBValidKey, +): Promise { + const index = store.index(indexName) + const records = await requestToPromise(index.getAll(IDBKeyRange.only(key))) + return records as T[] +} + +// 按索引值批量删除 +export function deleteByIndex( + store: IDBObjectStore, + indexName: string, + key: IDBValidKey, +): Promise { + return new Promise((resolve, reject) => { + const index = store.index(indexName) + const request = index.openCursor(IDBKeyRange.only(key)) + + request.onerror = () => { + reject(request.error ?? new Error('Failed to iterate index cursor')) + } + + request.onsuccess = (event) => { + const cursor = (event.target as IDBRequest).result + if (!cursor) { + resolve() + return + } + + cursor.delete() + cursor.continue() + } + }) +} diff --git a/packages/core/src/request/adapters/indexeddb/utils/migrations.ts b/packages/core/src/request/adapters/indexeddb/utils/migrations.ts new file mode 100644 index 0000000..a8b74cf --- /dev/null +++ b/packages/core/src/request/adapters/indexeddb/utils/migrations.ts @@ -0,0 +1,73 @@ +import type { LegacyCanvasRecord, LegacyComponentRecord } from '../../../types' + +// 安全转对象:用于处理历史数据中不可靠的字段类型 +function toObjectRecord(value: unknown): Record | undefined { + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + return undefined + } + return { ...(value as Record) } +} + +// 画布结构迁移(v1 -> v2) +export function migrateCanvasRecordV1ToV2(record: LegacyCanvasRecord): LegacyCanvasRecord { + const migratedRecord: LegacyCanvasRecord = { ...record } + + const nextConfig = toObjectRecord(migratedRecord.config) + ?? toObjectRecord(migratedRecord.configs) + ?? {} + + if (migratedRecord.viewport !== undefined && nextConfig.viewport === undefined) { + nextConfig.viewport = migratedRecord.viewport + } + + if (migratedRecord.grid !== undefined && nextConfig.grid === undefined) { + nextConfig.grid = migratedRecord.grid + } + + const nextStyle = toObjectRecord(nextConfig.style) ?? {} + if (migratedRecord.background !== undefined && nextStyle.background === undefined) { + nextStyle.background = migratedRecord.background + } + + if (Object.keys(nextStyle).length > 0) { + nextConfig.style = nextStyle + } + + if (Object.keys(nextConfig).length > 0) { + migratedRecord.config = nextConfig + } + + delete migratedRecord.configs + delete migratedRecord.background + delete migratedRecord.grid + delete migratedRecord.viewport + + if (!migratedRecord.type) { + migratedRecord.type = 'canvas' + } + + // 新版本 vars 统一要求数组 + if (!Array.isArray(migratedRecord.vars)) { + migratedRecord.vars = [] + } + + return migratedRecord +} + +// 组件结构迁移(v1 -> v2) +export function migrateComponentRecordV1ToV2(record: LegacyComponentRecord): LegacyComponentRecord { + const migratedRecord: LegacyComponentRecord = { ...record } + const component = toObjectRecord(migratedRecord.component) + + if (!component) { + return migratedRecord + } + + if (component.config === undefined && component.configs !== undefined) { + component.config = component.configs + } + delete component.configs + + migratedRecord.component = component + return migratedRecord +} diff --git a/packages/core/src/request/client.ts b/packages/core/src/request/client.ts new file mode 100644 index 0000000..4b2ca29 --- /dev/null +++ b/packages/core/src/request/client.ts @@ -0,0 +1,107 @@ +import type { FetchOptions } from 'ofetch' +import type { CanvasRepository } from './types/canvas.repository' +import type { ProjectRepository } from './types/project.repository' +import { ElMessage } from 'element-plus' +import { ofetch } from 'ofetch' +import pinia, { useAppStore } from '@/stores' +import { clearAuthStorage, clearDCSStorage, delayTimer } from '@/utils' +import { HttpCanvasRepository } from './adapters/http/canvas-repository' +import { HttpProjectRepository } from './adapters/http/project-repository' +import { IndexedDBCanvasRepository } from './adapters/indexeddb/canvas-repository' +import { IndexedDBProjectRepository } from './adapters/indexeddb/project-repository' +import { checkRefreshTokenAndReturn } from './util' + +// 远端请求配置(后续切换服务端接口直接复用) +export interface RequestConfig extends FetchOptions { + baseURL?: string + timeout?: number +} + +const DEFAULT_REQUEST_CONFIG: RequestConfig = { + baseURL: '/cslab-server', + timeout: 1000 * 60, +} + +// 统一 HTTP 客户端工厂 +export function createHttpClient(config: RequestConfig = {}) { + const mergedConfig: RequestConfig = { + ...DEFAULT_REQUEST_CONFIG, + ...config, + } + + return ofetch.create({ + baseURL: mergedConfig.baseURL, + timeout: mergedConfig.timeout, + async onRequest({ options }) { + // 默认 JSON 请求头 + const headers = new Headers(options.headers) + if (!headers.has('Content-Type')) { + headers.set('Content-Type', 'application/json') + } + + if (useAppStore(pinia).isWeb) { + options.headers = await checkRefreshTokenAndReturn(headers) + } + else { + options.headers = headers + } + }, + onResponse({ response, options }) { + const { data, msg, status } = response._data + if (status !== 200) { + if (status === 401) { + ElMessage.error('登录状态已过期,请重新登录') + clearAuthStorage() + clearDCSStorage() + delayTimer(500).then(() => { + location.replace(location.origin) + }) + return + } + + // 统一处理业务错误提示 + if (!options.headers.get('Hidden-Error')) { + ElMessage.error(msg || '请求出错') + } + + throw new Error(response._data) + } + + response._data = data + }, + onRequestError({ error }) { + console.error('[request:onRequestError]', error) + }, + onResponseError({ response }) { + // 统一抛出标准 Error,简化调用层处理 + const message = response._data?.message || `HTTP Error: ${response.status}` + throw new Error(message) + }, + }) +} + +export interface RequestClient { + project: ProjectRepository + canvas: CanvasRepository +} + +export function createRequestClient() { + const local: RequestClient = { + project: new IndexedDBProjectRepository(), + canvas: new IndexedDBCanvasRepository(), + } + + const fetcher = createHttpClient() + const http: RequestClient = { + project: new HttpProjectRepository({ fetcher }), + canvas: new HttpCanvasRepository({ fetcher }), + } + + return { + local, + http, + } +} + +// 导出客户端实例 +export const requestClient = createRequestClient() diff --git a/packages/core/src/request/constants.ts b/packages/core/src/request/constants.ts new file mode 100644 index 0000000..0500376 --- /dev/null +++ b/packages/core/src/request/constants.ts @@ -0,0 +1,18 @@ +// IndexedDB 数据库标识与版本号 +export const DCS_DB_NAME = 'cslab-dcs-editor' +export const DCS_DB_VERSION = 2 + +// 统一的对象仓库名称,避免硬编码字符串 +export const DCS_STORE_NAMES = { + projects: 'projects', // 仅本地exe使用 + canvases: 'canvases', // 画布 + components: 'components', // 画布组件 + layers: 'layers', // 画布图层 + logs: 'logs', // 编辑与运行日志 + meta: 'meta', // 元信息 + history: 'history', // 历史操作 +} as const + +// 本地项目(离线模式) +export const DEFAULT_LOCAL_PROJECT_ID = 'dcs-local-project' +export const DEFAULT_LOCAL_PROJECT_NAME = '本地项目' diff --git a/packages/core/src/request/index.ts b/packages/core/src/request/index.ts new file mode 100644 index 0000000..f193c18 --- /dev/null +++ b/packages/core/src/request/index.ts @@ -0,0 +1,13 @@ +/** + * 请求与数据存储层统一导出 + */ + +export * from './adapters/http/canvas-repository' +export * from './adapters/http/project-repository' + +export * from './adapters/indexeddb/canvas-repository' +export * from './adapters/indexeddb/project-repository' +export * from './client' +export * from './constants' + +export * from './types' diff --git a/packages/core/src/request/types/canvas.repository.ts b/packages/core/src/request/types/canvas.repository.ts new file mode 100644 index 0000000..75eb987 --- /dev/null +++ b/packages/core/src/request/types/canvas.repository.ts @@ -0,0 +1,34 @@ +import type { + CanvasDetail, + CanvasSummary, + CreateCanvasInput, + DeleteCanvasInput, + DuplicateCanvasInput, + ReorderCanvasesInput, + SaveCanvasComponentsInput, + UpdateCanvasBaseLayerInput, + UpdateCanvasInput, +} from './canvas.types' + +// 画布数据仓库接口: +// 当前可由 IndexedDB 或 HTTP 实现,业务层只依赖本接口 +export interface CanvasRepository { + // 查询指定项目下的画布列表 + list: (projectId: string) => Promise + // 查询单个画布详情(含组件) + getById: (canvasId: string) => Promise + // 新建画布 + create: (input: CreateCanvasInput) => Promise + // 重命名画布 + update: (input: UpdateCanvasInput) => Promise + // 复制画布(含组件) + duplicate: (input: DuplicateCanvasInput) => Promise + // 删除画布(级联删除组件/日志) + remove: (input: DeleteCanvasInput) => Promise + // 覆盖保存画布组件快照 + updateComponentsLayer: (input: SaveCanvasComponentsInput) => Promise + // 更新底图/背景图层配置 + updateBaseLayer: (input: UpdateCanvasBaseLayerInput) => Promise + // 批量更新画布排序 + reorder: (input: ReorderCanvasesInput) => Promise +} diff --git a/packages/core/src/request/types/canvas.types.ts b/packages/core/src/request/types/canvas.types.ts new file mode 100644 index 0000000..be50152 --- /dev/null +++ b/packages/core/src/request/types/canvas.types.ts @@ -0,0 +1,80 @@ +import type { Canvas, CanvasConfig, CanvasType, Layer, Variable } from '@cslab-dcs/schema' + +// 画布组件持久化结构: +// 与 UI 层解耦,避免 request 直接依赖具体组件类型实现 + +// 画布列表项(轻量) +export interface CanvasSummary { + id: string + projectId: string + name: string + description: string + thumbnail?: string + updatedAt: string + createdAt: string + order: number +} + +// 画布详情(含组件快照) +export interface CanvasDetail extends CanvasSummary { + width: number + height: number + type: CanvasType + version: Canvas['version'] + config?: CanvasConfig + vars?: Variable[] + components: Layer[] +} + +// 新建画布参数 +export interface CreateCanvasInput { + projectId: string + name: string + description?: string + thumbnail?: string + width?: number + height?: number + config?: CanvasConfig + vars?: Variable[] +} + +// 重命名参数 +export interface UpdateCanvasInput { + canvasId: string + name: string + description?: string +} + +// 复制参数 +export interface DuplicateCanvasInput { + canvasId: string + targetProjectId?: string + name?: string +} + +// 删除参数 +export interface DeleteCanvasInput { + canvasId: string +} + +// 保存组件快照参数 +export interface SaveCanvasComponentsInput { + canvasId: string + components: Layer[] +} + +// 画布排序参数 +export interface ReorderCanvasesInput { + projectId: string + canvasIds: string[] // 按目标顺序排列的画布 ID 列表 +} + +// 更新底图参数(背景图层) +export interface UpdateCanvasBaseLayerInput { + canvasId: string + width: number + height: number + thumbnail?: string | null + lockAspectRatio?: boolean + backgroundColor?: string +} diff --git a/packages/core/src/request/types/index.ts b/packages/core/src/request/types/index.ts new file mode 100644 index 0000000..bda414c --- /dev/null +++ b/packages/core/src/request/types/index.ts @@ -0,0 +1,5 @@ +export * from './canvas.repository' +export * from './canvas.types' +export * from './project.repository' +export * from './project.types' +export * from './shared.types' diff --git a/packages/core/src/request/types/project.repository.ts b/packages/core/src/request/types/project.repository.ts new file mode 100644 index 0000000..41f2fab --- /dev/null +++ b/packages/core/src/request/types/project.repository.ts @@ -0,0 +1,15 @@ +import type { CreateProjectInput, DeleteProjectInput, ProjectSummary, UpdateProjectInput } from './project.types' + +// 项目仓库接口 +export interface ProjectRepository { + // 查询项目列表 + list: () => Promise + // 查询单个项目 + getById: (projectId: string) => Promise + // 新建项目 + create?: (input: CreateProjectInput) => Promise + // 重命名项目 + update?: (input: UpdateProjectInput) => Promise + // 删除项目(通常伴随级联删除) + remove?: (input: DeleteProjectInput) => Promise +} diff --git a/packages/core/src/request/types/project.types.ts b/packages/core/src/request/types/project.types.ts new file mode 100644 index 0000000..685ddab --- /dev/null +++ b/packages/core/src/request/types/project.types.ts @@ -0,0 +1,33 @@ +import type { ISODateTimeString } from './shared.types' + +// 项目列表/详情基础结构 +export interface ProjectSummary { + project_pk: string + name: string + code: string + describe: string + is_public: '0' | '1' + create_timestamp: ISODateTimeString + base_pro: string +} + +// 新建项目参数 +export interface CreateProjectInput { + name: string + code: string + describe: string + classify: string + guide_code: '017' + assocPro: string +} + +// 项目重命名参数 +export interface UpdateProjectInput { + pk: string + parameter: { name: string, describe: string } +} + +// 删除项目参数 +export interface DeleteProjectInput { + pk: string +} diff --git a/packages/core/src/request/types/shared.types.ts b/packages/core/src/request/types/shared.types.ts new file mode 100644 index 0000000..21bdd61 --- /dev/null +++ b/packages/core/src/request/types/shared.types.ts @@ -0,0 +1,151 @@ +import type { Canvas, ComponentEventTrigger, Layer } from '@cslab-dcs/schema' + +// 统一时间字符串类型(ISO 8601) +export type ISODateTimeString = string + +// request 层支持的数据源类型 +export type RequestAdapter = 'indexeddb' | 'http' + +// 项目记录(projects store) +export interface DCSProjectRecord { + id: string + name: string + describe?: string + code: string + base_pro: string + createdAt: ISODateTimeString + updatedAt: ISODateTimeString +} + +// 画布记录(canvases store) +export interface DCSCanvasRecord { + id: string + projectId: string + name: string + description?: string + thumbnail?: string + width: number + height: number + type: Canvas['type'] + version: Canvas['version'] + config?: Canvas['config'] + vars?: Canvas['vars'] + order: number + createdAt: ISODateTimeString + updatedAt: ISODateTimeString +} + +// 组件记录(components store) +export interface DCSComponentRecord { + id: string + projectId: string + canvasId: string + component: Layer + // 组件图层顺序,数值越小越靠底层(背景图层不在本表中) + order?: number + createdAt: ISODateTimeString + updatedAt: ISODateTimeString +} + +// 编辑态日志结构(符合 PRD 字段) +export interface EditorLogPayload { + mode: 'editor' + time: ISODateTimeString + action: string + target?: string + detail?: string + result?: string + operator?: string +} + +// 运行态日志结构(符合 PRD 字段) +export interface RuntimeLogPayload { + mode: 'runtime' + time: ISODateTimeString + level: 'INFO' | 'WARN' | 'ERROR' | 'DEBUG' | 'AUDIT' + category?: string + message: string + variable?: string + value?: unknown + quality?: 'GOOD' | 'BAD' | 'UNCERTAIN' + traceId?: string +} + +export type DCSLogPayload = EditorLogPayload | RuntimeLogPayload + +// 日志记录(logs store) +export interface DCSLogRecord { + id: string + projectId: string + canvasId?: string + payload: DCSLogPayload + createdAt: ISODateTimeString +} + +// 元信息记录(meta store) +export interface DCSMetaRecord { + key: string + value: unknown + updatedAt: ISODateTimeString +} + +// IndexedDB 各 store 的记录映射 +export interface DCSDatabaseSchema { + projects: DCSProjectRecord + canvases: DCSCanvasRecord + components: DCSComponentRecord + logs: DCSLogRecord + meta: DCSMetaRecord +} + +export type DCSStoreName = keyof DCSDatabaseSchema + +// 预留:统一创建记录时的上下文参数 +export interface CreateRecordContext { + now: ISODateTimeString + projectId: string +} + +// v1 旧画布结构(用于升级迁移) +export interface LegacyCanvasRecord { + [key: string]: unknown + id?: string + projectId?: string + name?: string + description?: string + thumbnail?: string + width?: number + height?: number + type?: string + version?: string + config?: unknown + order?: number + createdAt?: ISODateTimeString + updatedAt?: ISODateTimeString + background?: unknown + grid?: unknown + viewport?: unknown + configs?: unknown + vars?: unknown +} + +// v1 旧组件结构(用于升级迁移) +export interface LegacyComponentLike extends Record { + config?: unknown + configs?: unknown + events?: Array<{ + trigger?: ComponentEventTrigger + [key: string]: unknown + }> +} + +// v1 旧组件记录结构(用于升级迁移) +export interface LegacyComponentRecord { + [key: string]: unknown + id?: string + projectId?: string + canvasId?: string + createdAt?: ISODateTimeString + updatedAt?: ISODateTimeString + component?: LegacyComponentLike +} diff --git a/packages/core/src/request/util.ts b/packages/core/src/request/util.ts new file mode 100644 index 0000000..3c70c6d --- /dev/null +++ b/packages/core/src/request/util.ts @@ -0,0 +1,112 @@ +import pinia, { useUserStore } from '@/stores' + +// 检查token是否过期 +export function checkRefreshToken(headers: Headers) { + const userStore = useUserStore(pinia) + + const { token, deviceType, rtokenTime } = userStore + if (!token) { + return headers + } + + if (Date.now() <= Number(rtokenTime)) { + headers.set('Authorization', `jwt ${token}`) + headers.set('DEVICE-TYPE', deviceType || 'pc') + + return headers + } + + return null +} + +// 刷新token +export async function refreshToken(headers: Headers) { + const userStore = useUserStore(pinia) + + try { + const refreshResponse = await fetch('/cslab-server/auth/refresh/', { + method: 'GET', + headers: { + 'Authorization': `jwt ${userStore.rtoken}`, + 'DEVICE-TYPE': userStore.deviceType || 'pc', + }, + }) + + const payloadText = await refreshResponse.text() + let payload: any = null + + if (payloadText) { + try { + payload = JSON.parse(payloadText) + } + catch (error) { + console.warn('[request] refresh token response is not valid JSON', error) + } + } + + const nextToken = payload?.data?.token ?? payload?.token + const nextRToken = payload?.data?.rtoken ?? payload?.rtoken + const success = refreshResponse.ok && (payload?.status === 200 || Boolean(nextToken)) + + if (success && nextToken && nextRToken) { + userStore.setToken(nextToken) + userStore.setRToken(nextRToken) + userStore.setRTokenTime(Date.now() + 12 * 60 * 1000) + } + } + finally { + if (userStore.token) { + headers.set('Authorization', `jwt ${userStore.token}`) + } + else { + headers.delete('Authorization') + } + headers.set('DEVICE-TYPE', userStore.deviceType || 'pc') + } + + return headers +} + +// 缓存正在进行的刷新请求,避免并发请求时多次刷新 +let refreshPromise: Promise | null = null + +// 检查并刷新token后返回结果 +export async function checkRefreshTokenAndReturn(headers: Headers) { + const checked = checkRefreshToken(headers) + + if (checked) { + return checked + } + + // 如果已经有刷新请求在进行中,等待它完成后重新检查 + if (refreshPromise) { + await refreshPromise + // 刷新完成后,重新检查 token(此时应该已经是新的了) + const rechecked = checkRefreshToken(headers) + if (rechecked) { + return rechecked + } + + // 如果仍然过期(极端情况),使用旧 token + + const userStore = useUserStore(pinia) + + const { token, deviceType } = userStore + headers.set('Authorization', `jwt ${token}`) + headers.set('DEVICE-TYPE', deviceType || 'pc') + + return headers + } + + // 第一个检测到过期的请求,发起刷新 + refreshPromise = refreshToken(headers) + + try { + const result = await refreshPromise + return result + } + finally { + // 刷新完成,清除缓存 + refreshPromise = null + } +} diff --git a/packages/core/src/router/index.ts b/packages/core/src/router/index.ts new file mode 100644 index 0000000..1053b63 --- /dev/null +++ b/packages/core/src/router/index.ts @@ -0,0 +1,237 @@ +import type { RouteRecordRaw } from 'vue-router' +import { createRouter, createWebHashHistory } from 'vue-router' +import pinia, { useAppStore, useCanvasStore, useProjectStore, useUserStore } from '@/stores' +import { delayTimer } from '@/utils' +import { getRouteCanvasId, getRouteProjectId, mergeRouteStateQuery, shouldAttachCanvasId } from './route-state' +import { ExeRoutes, WebRoutes } from './routes' + +const Layout = () => import('../layout') + +const routes: RouteRecordRaw[] = [ + { + path: '/', + name: 'DCS', + component: Layout, + children: [], + }, +] + +const router = createRouter({ + history: createWebHashHistory('/dcs-web'), // 使用 Hash 模式兼容 Electron + routes, +}) + +export default router + +export function generateRoutes(isWeb: boolean) { + if (isWeb) { + ExeRoutes.forEach((route) => { + if (router.hasRoute(route.name!)) { + router.removeRoute(route.name!) + } + }) + + WebRoutes.forEach((route) => { + if (!router.hasRoute(route.name!)) { + router.addRoute('DCS', route) + } + }) + } + else { + WebRoutes.forEach((route) => { + if (router.hasRoute(route.name!)) { + router.removeRoute(route.name!) + } + }) + + ExeRoutes.forEach((route) => { + if (!router.hasRoute(route.name!)) { + router.addRoute('DCS', route) + } + }) + } + + router.resolve({ name: 'DCS' }) +} + +async function awaitPlatformInfoReady() { + const appStore = useAppStore(pinia) + + if (appStore.platformInfo.name) + return + + if (!appStore.platformInfo.name) { + await delayTimer(100) + await awaitPlatformInfoReady() + } +} + +// 保存查询参数 +function saveQueryState() { + const appStore = useAppStore(pinia) + const query = Object.fromEntries(new URLSearchParams(window.location.search)) as Record + const { token, rtoken, rtokenTime, deviceType, projectId, canvasId, ...reset } = query + + const hasSensitiveQuery = Boolean(token || rtoken || rtokenTime || deviceType || projectId || canvasId) + + if (!appStore.isWeb || !hasSensitiveQuery) { + return { + query: reset, + replaced: false, + } + } + + const userStore = useUserStore(pinia) + + if (token) + userStore.setToken(token) + + if (rtoken) + userStore.setRToken(rtoken) + + if (rtokenTime) + userStore.setRTokenTime(Number(rtokenTime)) + + if (deviceType) + userStore.setDeviceType(deviceType) + + if (projectId) { + const projectStore = useProjectStore(pinia) + projectStore.setProjectId(projectId) + } + + if (canvasId) { + const canvasStore = useCanvasStore(pinia) + canvasStore.setCanvasId(canvasId) + } + + // 将项目/画布上下文转移到 hash 路由查询中,保持 URL 持续可见。 + const rawHash = location.hash.startsWith('#') ? location.hash.slice(1) : location.hash + const [hashPath = '/', hashQueryString = ''] = rawHash.split('?') + const hashQuery = Object.fromEntries(new URLSearchParams(hashQueryString)) + const includeCanvasId = shouldAttachCanvasId({ path: hashPath }) + const nextHashQuery = new URLSearchParams( + mergeRouteStateQuery(hashQuery, { projectId, canvasId }, { includeCanvasId }) as Record, + ).toString() + + let url = location.origin + location.pathname + const queryStr = new URLSearchParams(reset).toString() + if (queryStr) + url += `?${queryStr}` + + url += `#${hashPath || '/'}` + if (nextHashQuery) + url += `?${nextHashQuery}` + + location.replace(url) + return { + query: reset, + replaced: true, + } +} + +router.beforeEach(async (to, _from, next) => { + await awaitPlatformInfoReady() + + // 保存查询参数 + const queryState = saveQueryState() + if (queryState.replaced) { + next(false) + return + } + + // 生成路由 + const appStore = useAppStore(pinia) + generateRoutes(appStore.isWeb) + + const projectStore = useProjectStore(pinia) + const canvasStore = useCanvasStore(pinia) + const includeCanvasId = shouldAttachCanvasId(to) + + const routeProjectId = getRouteProjectId(to.query) + const routeCanvasId = getRouteCanvasId(to.query) + + if (routeProjectId) { + if (projectStore.projectId !== routeProjectId) { + projectStore.setProjectId(routeProjectId) + } + } + else if (!projectStore.projectId) { + await projectStore.getProjectList() + const fallbackProjectId = projectStore.projectList[0]?.project_pk || '' + if (fallbackProjectId) { + projectStore.setProjectId(fallbackProjectId) + next({ + ...to, + query: mergeRouteStateQuery(to.query, { + projectId: fallbackProjectId, + canvasId: canvasStore.canvasId, + }, { includeCanvasId }), + replace: true, + }) + return + } + } + + if (includeCanvasId && routeCanvasId && canvasStore.canvasId !== routeCanvasId) { + canvasStore.setCanvasId(routeCanvasId) + } + + const normalizedProjectId = projectStore.projectId || '' + const normalizedCanvasId = canvasStore.canvasId || '' + if ( + routeProjectId !== normalizedProjectId + || (includeCanvasId && routeCanvasId !== normalizedCanvasId) + ) { + next({ + ...to, + query: mergeRouteStateQuery(to.query, { + projectId: normalizedProjectId, + canvasId: normalizedCanvasId, + }, { includeCanvasId }), + replace: true, + }) + return + } + + if (router.hasRoute(to.name!) && to.name !== 'DCS') { + next() + } + else { + // 尝试重新解析目标路由(应对动态路由刷新时初次解析失败的情况) + const resolved = router.resolve(to.fullPath) + if (resolved.name && resolved.name !== 'DCS' && resolved.matched.length > 0) { + next({ + ...to, + query: mergeRouteStateQuery(queryState.query, { + projectId: projectStore.projectId, + canvasId: canvasStore.canvasId, + }, { includeCanvasId }), + replace: true, + }) + return + } + + if (appStore.isWeb) { + next({ + name: 'Canvases', + query: mergeRouteStateQuery(queryState.query, { + projectId: projectStore.projectId, + canvasId: canvasStore.canvasId, + }, { includeCanvasId: false }), + params: to.params, + replace: true, + }) + } + else { + next({ + name: 'Welcome', + query: mergeRouteStateQuery(queryState.query, { + projectId: projectStore.projectId, + canvasId: canvasStore.canvasId, + }, { includeCanvasId: false }), + replace: true, + }) + } + } +}) diff --git a/packages/core/src/router/route-state.ts b/packages/core/src/router/route-state.ts new file mode 100644 index 0000000..22ddf8d --- /dev/null +++ b/packages/core/src/router/route-state.ts @@ -0,0 +1,55 @@ +import type { LocationQuery, LocationQueryRaw, LocationQueryValue, RouteLocationNormalized, RouteLocationNormalizedLoaded } from 'vue-router' + +export interface RouteStateInput { + projectId?: string | null + canvasId?: string | null +} + +type RouteLike = Pick | Pick + +function normalizeQueryValue(value: LocationQueryValue | LocationQueryValue[] | undefined): string { + if (Array.isArray(value)) { + return value.find(item => !!item)?.trim() || '' + } + + return value?.trim() || '' +} + +export function getRouteProjectId(query: LocationQuery): string { + return normalizeQueryValue(query.projectId) +} + +export function getRouteCanvasId(query: LocationQuery): string { + return normalizeQueryValue(query.canvasId) +} + +export function shouldAttachCanvasId(route: RouteLike | { path: string, name?: string | symbol | null }): boolean { + const name = route.name + const path = route.path + return name === 'Editor' || name === 'Runtime' || name === 'Preview' + || path === '/editor' || path === '/runtime' || path === '/preview' +} + +export function mergeRouteStateQuery( + query: LocationQuery | LocationQueryRaw, + state: RouteStateInput, + options?: { includeCanvasId?: boolean }, +): LocationQueryRaw { + const nextQuery: LocationQueryRaw = { ...query } + + if (state.projectId) { + nextQuery.projectId = state.projectId + } + else { + delete nextQuery.projectId + } + + if (options?.includeCanvasId !== false && state.canvasId) { + nextQuery.canvasId = state.canvasId + } + else { + delete nextQuery.canvasId + } + + return nextQuery +} diff --git a/packages/core/src/router/routes.ts b/packages/core/src/router/routes.ts new file mode 100644 index 0000000..404bf9b --- /dev/null +++ b/packages/core/src/router/routes.ts @@ -0,0 +1,61 @@ +import type { RouteRecordRaw } from 'vue-router' + +const previewRoute: RouteRecordRaw = { + path: '/preview', + name: 'Preview', + component: () => import('../views/preview.vue'), + meta: { title: '预览' }, +} + +const runtimeRoute: RouteRecordRaw = { + path: '/runtime', + name: 'Runtime', + component: () => import('../views/runtime.vue'), + meta: { title: '运行模式' }, +} + +export const WebRoutes: RouteRecordRaw[] = [ + { + path: '/canvases', + name: 'Canvases', + component: () => import('../views/canvases/list.vue'), + meta: { title: '画布列表' }, + }, + { + path: '/editor', + name: 'Editor', + component: () => import('../views/editor.vue'), + meta: { title: 'DCS编辑器' }, + }, + previewRoute, + runtimeRoute, +] + +export const ExeRoutes: RouteRecordRaw[] = [ + { + path: '/canvases', + name: 'Canvases', + component: () => import('../views/canvases/list.vue'), + meta: { title: '画布列表' }, + }, + { + path: '/editor', + name: 'Editor', + component: () => import('../views/editor.vue'), + meta: { title: 'DCS编辑器' }, + }, + previewRoute, + runtimeRoute, + { + path: '/settings', + name: 'Settings', + component: () => import('../views/stand-alone/settings.vue'), + meta: { title: 'DCS设置' }, + }, + { + path: '/welcome', + name: 'Welcome', + component: () => import('../views/stand-alone/welcome.vue'), + meta: { title: 'DCS欢迎页' }, + }, +] diff --git a/packages/core/src/stores/app.ts b/packages/core/src/stores/app.ts new file mode 100644 index 0000000..4d20d99 --- /dev/null +++ b/packages/core/src/stores/app.ts @@ -0,0 +1,33 @@ +import type { PlatformInfo } from '@cslab-dcs/bridge' +import { useLocalStorage } from '@vueuse/core' +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' + +export const useAppStore = defineStore('app', () => { + const theme = ref<'light' | 'dark'>('light') + + function toggleTheme() { + theme.value = theme.value === 'light' ? 'dark' : 'light' + } + + const platformInfo = useLocalStorage('DCS_PLATFORM_INFO', {} as PlatformInfo) + function setPlatformInfo(info: PlatformInfo) { + platformInfo.value = info + } + const appVersion = useLocalStorage('DCS_APP_VERSION', '') + function setAppVersion(version: string) { + appVersion.value = version + } + + const isWeb = computed(() => platformInfo.value?.name === 'web') + + return { + theme, + toggleTheme, + platformInfo, + setPlatformInfo, + appVersion, + setAppVersion, + isWeb, + } +}) diff --git a/packages/core/src/stores/canvas.ts b/packages/core/src/stores/canvas.ts new file mode 100644 index 0000000..9398b5b --- /dev/null +++ b/packages/core/src/stores/canvas.ts @@ -0,0 +1,90 @@ +import type { CanvasView } from '@cslab-dcs/schema' +import type { CanvasDetail, CanvasSummary } from '@/request' +import { useSessionStorage } from '@vueuse/core' +import { defineStore } from 'pinia' +import { ref, shallowReactive, shallowRef } from 'vue' +import { getCanvasByIdApi, listCanvasesApi } from '@/api' + +export const useCanvasStore = defineStore('canvas', () => { + // 画布列表 + const canvasList = shallowRef([]) + + // 获取画布列表 + async function getCanvasList() { + const canvases = await listCanvasesApi() + canvasList.value = canvases + return canvases + } + + // 统一外部修改入口,避免组件直接写入 refs + function setCanvasList(canvases: CanvasSummary[]) { + canvasList.value = [...canvases] + } + + // 当前编辑的画布ID + const canvasId = useSessionStorage('DCS_CANVAS_ID', null) + + // 设置当前编辑的画布ID + function setCanvasId(id: string | null) { + canvasId.value = id + } + + // 当前编辑的画布信息 + const canvasInfo = shallowRef(null) + + // 获取当前编辑的画布信息 + async function getCanvasInfo() { + if (!canvasId.value) { + canvasInfo.value = null + return + } + + canvasInfo.value = await getCanvasByIdApi(canvasId.value) + } + + // 画布缩放比例,默认100% + const canvasZoom = useSessionStorage('DCS_CANVAS_ZOOM', 100) + + // 修改画布缩放比例 + function setCanvasZoom(zoom: number) { + // 必须是正儿八经的数字 + if (Number.isFinite(zoom) && Number(zoom) > 0) { + canvasZoom.value = Number(zoom) + } + } + + // true 表示正在从持久层恢复数据,期间不触发反向保存 + const isHydratingCanvas = ref(false) + // 画布缩略图(用于持久化) + const activeCanvasThumbnail = ref('') + // 画布视图状态 画布 + 图片的关键指标(图片坐标系)。 + const canvasView = shallowReactive({ + canvasWidth: 0, + canvasHeight: 0, + imageWidth: 0, + imageHeight: 0, + imageNaturalWidth: 0, + imageNaturalHeight: 0, + backgroundLockAspectRatio: true, + backgroundAverageColor: null, + offsetX: 0, + offsetY: 0, + scale: 1, + background: '#FFFFFF', + }) + + return { + canvasList, + getCanvasList, + setCanvasList, + canvasId, + setCanvasId, + canvasInfo, + getCanvasInfo, + canvasZoom, + setCanvasZoom, + isHydratingCanvas, + activeCanvasThumbnail, + canvasView, + } +}) diff --git a/packages/core/src/stores/getters.ts b/packages/core/src/stores/getters.ts new file mode 100644 index 0000000..1ea42ac --- /dev/null +++ b/packages/core/src/stores/getters.ts @@ -0,0 +1,25 @@ +import type { CSSProperties } from 'vue' +import { storeToRefs } from 'pinia' +import { computed } from 'vue' +import { normalizeRect } from '@/components/editor/canvas/utils' +import pinia, { useCanvasStore, useLayerStore } from '.' + +export function useEditorStoreGetters() { + const { canvasView } = storeToRefs(useCanvasStore(pinia)) + const { boxSelect } = storeToRefs(useLayerStore(pinia)) + + const selectionBoxStyle = computed(() => { + if (!boxSelect.value.active || !boxSelect.value.moved || canvasView.value.scale <= 0) { + return null + } + const rect = normalizeRect(boxSelect.value.startX, boxSelect.value.startY, boxSelect.value.currentX, boxSelect.value.currentY) + return { + left: `${canvasView.value.offsetX + rect.x * canvasView.value.scale}px`, + top: `${canvasView.value.offsetY + rect.y * canvasView.value.scale}px`, + width: `${rect.width * canvasView.value.scale}px`, + height: `${rect.height * canvasView.value.scale}px`, + } + }) + + return { selectionBoxStyle } +} diff --git a/packages/core/src/stores/index.ts b/packages/core/src/stores/index.ts new file mode 100644 index 0000000..31b5e37 --- /dev/null +++ b/packages/core/src/stores/index.ts @@ -0,0 +1,11 @@ +import { createPinia } from 'pinia' + +export const pinia = createPinia() + +export * from './app' +export * from './canvas' +export * from './layer' +export * from './project' +export * from './user' + +export default pinia diff --git a/packages/core/src/stores/layer.ts b/packages/core/src/stores/layer.ts new file mode 100644 index 0000000..ae3082e --- /dev/null +++ b/packages/core/src/stores/layer.ts @@ -0,0 +1,320 @@ +import type { Layer } from '@cslab-dcs/schema' +import { useSessionStorage } from '@vueuse/core' +import { set } from 'es-toolkit/compat' +import { defineStore } from 'pinia' +import { computed, reactive, shallowRef } from 'vue' +import { getLayerGroupById } from '@/components/editor/canvas/grouping' +import { cloneData } from '@/utils' + +export const useLayerStore = defineStore('layer', () => { + const selectedLayerId = useSessionStorage('DCS_LAYER_ID', '') + const selectedLayerIds = useSessionStorage('DCS_LAYER_IDS', []) + const selectedGroupId = useSessionStorage('DCS_LAYER_GROUP_ID', '') + const isBaseLayerActive = useSessionStorage('DCS_BASE_LAYER_ACTIVE', false) + + function setLayerId(id: string) { + selectedLayerId.value = id + if (id && !selectedLayerIds.value.includes(id)) { + selectedGroupId.value = '' + } + if (id) { + isBaseLayerActive.value = false + } + } + + function setSelectedLayerIds(ids: string[], primaryId?: string | null, options?: { groupId?: string | null }) { + const nextIds = [...new Set(ids)].filter(Boolean) + selectedLayerIds.value = nextIds + selectedGroupId.value = options?.groupId ?? '' + const nextPrimary = primaryId && nextIds.includes(primaryId) + ? primaryId + : nextIds[0] ?? '' + selectedLayerId.value = nextPrimary + if (nextIds.length) { + isBaseLayerActive.value = false + } + } + + const layerList = shallowRef([]) + + function syncSelectionAfterLayerChange() { + const existsSet = new Set(layerList.value.map(item => item.id)) + const nextIds = selectedLayerIds.value.filter(id => existsSet.has(id)) + if (nextIds.length !== selectedLayerIds.value.length) { + selectedLayerIds.value = nextIds + } + if (selectedLayerId.value && !existsSet.has(selectedLayerId.value)) { + selectedLayerId.value = nextIds[0] ?? '' + } + + if (selectedGroupId.value) { + const group = getLayerGroupById(layerList.value, selectedGroupId.value) + const groupIds = group?.layerIds ?? [] + if (!group || groupIds.length !== nextIds.length || groupIds.some(id => !nextIds.includes(id))) { + selectedGroupId.value = '' + } + } + } + + function clearLayerSelection(options?: { preserveBaseLayer?: boolean }) { + setSelectedLayerIds([], null, { groupId: null }) + if (!options?.preserveBaseLayer) { + isBaseLayerActive.value = false + } + } + + function activateBaseLayerSelection() { + isBaseLayerActive.value = true + selectedLayerIds.value = [] + selectedLayerId.value = '' + selectedGroupId.value = '' + } + + function deactivateBaseLayerSelection() { + isBaseLayerActive.value = false + } + + function setLayerList(nextLayers: Layer[], options?: { preserveSelection?: boolean }) { + layerList.value = nextLayers.map(layer => cloneData(layer)) + if (options?.preserveSelection) { + syncSelectionAfterLayerChange() + return + } + clearLayerSelection() + } + + function replaceLayers(nextLayers: Layer[]) { + setLayerList(nextLayers) + } + + function appendLayers(nextLayers: Layer[]) { + if (!nextLayers.length) { + return + } + layerList.value = [ + ...layerList.value, + ...nextLayers.map(layer => cloneData(layer)), + ] + } + + function removeLayersByIds(layerIds: string[]) { + const targetSet = new Set(layerIds) + if (!targetSet.size) { + return + } + layerList.value = layerList.value.filter(item => !targetSet.has(item.id)) + syncSelectionAfterLayerChange() + } + + function updateLayerById(id: string, updater: (layer: Layer) => Layer) { + let changed = false + layerList.value = layerList.value.map((item) => { + if (item.id !== id) { + return item + } + changed = true + return updater(cloneData(item)) + }) + return changed + } + + function updateLayerRect({ + id, + nextX, + nextY, + nextWidth, + nextHeight, + options, + }: { + id: string + nextX: number + nextY: number + nextWidth: number + nextHeight: number + options?: { ignoreLock?: boolean } + }) { + updateLayerById(id, (item) => { + if (item.config?.locked && !options?.ignoreLock) { + return item + } + return { + ...item, + x: Math.round(nextX), + y: Math.round(nextY), + width: Math.round(nextWidth), + height: Math.round(nextHeight), + } + }) + } + + function updateLayerPosition({ id, nextX, nextY }: { id: string, nextX: number, nextY: number }) { + updateLayerById(id, (item) => { + if (item.config?.locked) { + return item + } + return { + ...item, + x: Math.round(nextX), + y: Math.round(nextY), + } + }) + } + + function updateLayerPositions(updates: Array<{ id: string, nextX: number, nextY: number }>) { + if (!updates.length) { + return + } + const updateMap = new Map( + updates.map(update => [ + update.id, + { + nextX: Math.round(update.nextX), + nextY: Math.round(update.nextY), + }, + ]), + ) + layerList.value = layerList.value.map((item) => { + const next = updateMap.get(item.id) + if (!next) { + return item + } + if (item.config?.locked) { + return item + } + return { + ...item, + x: next.nextX, + y: next.nextY, + } + }) + } + + function updateLayerRects( + updates: Array<{ id: string, nextX: number, nextY: number, nextWidth: number, nextHeight: number, options?: { ignoreLock?: boolean } }>, + ) { + if (!updates.length) { + return + } + const updateMap = new Map( + updates.map(update => [ + update.id, + { + nextX: Math.round(update.nextX), + nextY: Math.round(update.nextY), + nextWidth: Math.round(update.nextWidth), + nextHeight: Math.round(update.nextHeight), + options: update.options, + }, + ]), + ) + + layerList.value = layerList.value.map((item) => { + const next = updateMap.get(item.id) + if (!next) { + return item + } + if (item.config?.locked && !next.options?.ignoreLock) { + return item + } + return { + ...item, + x: next.nextX, + y: next.nextY, + width: next.nextWidth, + height: next.nextHeight, + } + }) + } + + function canMutateLockedLayerField(field: string) { + return field === 'config.locked' || field === 'config.visible' + } + + function updateLayerField(id: string, field: string, value: unknown, options?: { ignoreLock?: boolean }) { + updateLayerById(id, (item) => { + if (item.config?.locked && !options?.ignoreLock && !canMutateLockedLayerField(field)) { + return item + } + const nextItem = cloneData(item) + set(nextItem, field, value) + return nextItem + }) + } + + function updateLayerFields(ids: string[], field: string, value: unknown, options?: { ignoreLock?: boolean }) { + const targetSet = new Set(ids.filter(Boolean)) + if (!targetSet.size) { + return + } + + layerList.value = layerList.value.map((item) => { + if (!targetSet.has(item.id)) { + return item + } + if (item.config?.locked && !options?.ignoreLock && !canMutateLockedLayerField(field)) { + return item + } + const nextItem = cloneData(item) + set(nextItem, field, value) + return nextItem + }) + } + + const selectedLayer = computed(() => { + if (selectedLayerId.value) { + const byPrimary = layerList.value.find(item => item.id === selectedLayerId.value) + if (byPrimary) { + return byPrimary + } + } + const firstSelected = selectedLayerIds.value[0] + if (!firstSelected) { + return null + } + return layerList.value.find(item => item.id === firstSelected) ?? null + }) + + const selectedGroup = computed(() => { + if (!selectedGroupId.value) { + return null + } + return getLayerGroupById(layerList.value, selectedGroupId.value) + }) + + const boxSelect = reactive({ + active: false, + additive: false, + moved: false, + startX: 0, + startY: 0, + currentX: 0, + currentY: 0, + }) + + return { + clearLayerSelection, + activateBaseLayerSelection, + deactivateBaseLayerSelection, + selectedLayerId, + selectedLayerIds, + selectedGroupId, + isBaseLayerActive, + setSelectedLayerIds, + selectedLayer, + selectedGroup, + setLayerId, + layerList, + setLayerList, + replaceLayers, + appendLayers, + removeLayersByIds, + updateLayerById, + updateLayerRect, + updateLayerPosition, + updateLayerPositions, + updateLayerRects, + updateLayerField, + updateLayerFields, + boxSelect, + } +}) diff --git a/packages/core/src/stores/project.ts b/packages/core/src/stores/project.ts new file mode 100644 index 0000000..e039549 --- /dev/null +++ b/packages/core/src/stores/project.ts @@ -0,0 +1,52 @@ +import type { ProjectSummary } from '@/request' +import { useSessionStorage } from '@vueuse/core' +import { defineStore } from 'pinia' +import { computed, shallowRef } from 'vue' +import { getProjectListApi } from '@/api' + +export const useProjectStore = defineStore('project', () => { + const projectId = useSessionStorage('DCS_PROJECT_ID', '') + + function setProjectId(id: string) { + projectId.value = id + } + + // 项目列表 + const projectList = shallowRef([]) + + // 当前编辑的项目信息 + const projectInfo = computed(() => { + return projectList.value.find(p => p.project_pk === projectId.value) || null + }) + + // 最近编辑的项目列表,排除当前编辑的项目,最多显示5个 + const recentProjects = computed(() => { + return projectList.value.filter(p => p.project_pk !== projectId.value).slice(0, 5) + }) + + // 获取项目列表(并发调用去重,多处同时调用只发一次请求) + let _pending: Promise | null = null + async function getProjectList() { + if (_pending) + return _pending + _pending = getProjectListApi() + .then((data) => { projectList.value = data }) + .finally(() => { _pending = null }) + return _pending + } + + // 获取项目信息 + async function getProjectInfo() { + await getProjectList() + } + + return { + projectId, + setProjectId, + projectList, + projectInfo, + getProjectInfo, + getProjectList, + recentProjects, + } +}) diff --git a/packages/core/src/stores/user.ts b/packages/core/src/stores/user.ts new file mode 100644 index 0000000..3adef15 --- /dev/null +++ b/packages/core/src/stores/user.ts @@ -0,0 +1,39 @@ +import { useLocalStorage } from '@vueuse/core' +import { defineStore } from 'pinia' + +export const useUserStore = defineStore('user', () => { + const token = useLocalStorage('CSLAB_TOKEN', '') + + function setToken(t: string) { + token.value = t + } + + const rtoken = useLocalStorage('CSLAB_RTOKEN', '') + + function setRToken(rt: string) { + rtoken.value = rt + } + + const rtokenTime = useLocalStorage('CSLAB_RTOKENTIME', 0) + + function setRTokenTime(time: number) { + rtokenTime.value = time + } + + const deviceType = useLocalStorage('CSLAB_DEVICE_TYPE', 'pc') + + function setDeviceType(type: string) { + deviceType.value = type + } + + return { + token, + setToken, + rtoken, + setRToken, + rtokenTime, + setRTokenTime, + deviceType, + setDeviceType, + } +}) diff --git a/packages/core/src/utils/auth.ts b/packages/core/src/utils/auth.ts new file mode 100644 index 0000000..0690bc3 --- /dev/null +++ b/packages/core/src/utils/auth.ts @@ -0,0 +1,48 @@ +/** + * 解析 JWT payload,提取 user_id 等字段 + */ +const RE_BASE64_MINUS = /-/g +const RE_BASE64_UNDERSCORE = /_/g + +export function parseJwtPayload(token: string): Record | null { + try { + const parts = token.split('.') + if (parts.length !== 3) + return null + const payload = atob(parts[1].replace(RE_BASE64_MINUS, '+').replace(RE_BASE64_UNDERSCORE, '/')) + return JSON.parse(payload) + } + catch { + return null + } +} + +export function getUserIdFromToken(token: string): number | null { + const payload = parseJwtPayload(token) + if (!payload || typeof payload.id !== 'number') + return null + return payload.id +} + +export function clearAuthStorage() { + const authKeys = ['CSLAB_TOKEN', 'CSLAB_RTOKEN', 'CSLAB_RTOKENTIME', 'CSLAB_DEVICE_TYPE'] + for (const key of authKeys) { + localStorage.removeItem(key) + } +} + +export function clearDCSStorage() { + for (let index = localStorage.length - 1; index >= 0; index -= 1) { + const key = localStorage.key(index) + if (key && key.startsWith('DCS_')) { + localStorage.removeItem(key) + } + } + + for (let index = sessionStorage.length - 1; index >= 0; index -= 1) { + const key = sessionStorage.key(index) + if (key && key.startsWith('DCS_')) { + sessionStorage.removeItem(key) + } + } +} diff --git a/packages/core/src/utils/canvas.ts b/packages/core/src/utils/canvas.ts new file mode 100644 index 0000000..3124023 --- /dev/null +++ b/packages/core/src/utils/canvas.ts @@ -0,0 +1,31 @@ +import type { Layer } from '@cslab-dcs/schema' + +/** + * 运行时校验:判断输入值是否满足画布组件图层的最小结构。 + * 目的:防止接口/持久层中的脏数据进入编辑态逻辑。 + */ +export function isLayerPayload(value: unknown): value is Layer { + if (!value || typeof value !== 'object') { + return false + } + + const layer = value as Partial & Record + + return typeof layer.id === 'string' + && typeof layer.type === 'string' + && typeof layer.x === 'number' + && typeof layer.y === 'number' + && typeof layer.width === 'number' + && typeof layer.height === 'number' +} + +/** + * 从未知数组中提取合法的画布组件图层数据。 + * 说明:仅做结构过滤,不做业务字段修正。 + */ +export function extractLayers(values: unknown[] | null | undefined): Layer[] { + if (!Array.isArray(values)) { + return [] + } + return values.filter(isLayerPayload) +} diff --git a/packages/core/src/utils/date.ts b/packages/core/src/utils/date.ts new file mode 100644 index 0000000..6eecd53 --- /dev/null +++ b/packages/core/src/utils/date.ts @@ -0,0 +1,123 @@ +import type { ConfigType, Dayjs, ManipulateType, OpUnitType } from 'dayjs' +/** + * 日期时间工具 + * 基于 dayjs 封装 + */ +import dayjs from 'dayjs' +import duration from 'dayjs/plugin/duration' +import relativeTime from 'dayjs/plugin/relativeTime' +import timezone from 'dayjs/plugin/timezone' +import utc from 'dayjs/plugin/utc' +import 'dayjs/locale/zh-cn' + +// 扩展插件 +dayjs.extend(relativeTime) +dayjs.extend(duration) +dayjs.extend(utc) +dayjs.extend(timezone) + +// 设置中文 +dayjs.locale('zh-cn') + +/** + * 默认日期格式 + */ +export const DATE_FORMAT = 'YYYY-MM-DD' +export const DATETIME_FORMAT = 'YYYY-MM-DD HH:mm:ss' +export const TIME_FORMAT = 'HH:mm:ss' + +/** + * 格式化日期 + */ +export function formatDate(date?: ConfigType, format = DATE_FORMAT): string { + return dayjs(date).format(format) +} + +/** + * 格式化日期时间 + */ +export function formatDateTime(date?: ConfigType, format = DATETIME_FORMAT): string { + return dayjs(date).format(format) +} + +/** + * 格式化时间 + */ +export function formatTime(date?: ConfigType, format = TIME_FORMAT): string { + return dayjs(date).format(format) +} + +/** + * 获取相对时间(如:3分钟前) + */ +export function fromNow(date: ConfigType): string { + return dayjs(date).fromNow() +} + +/** + * 获取到现在的相对时间 + */ +export function toNow(date: ConfigType): string { + return dayjs(date).toNow() +} + +/** + * 日期加减 + */ +export function addTime(date: ConfigType, value: number, unit: ManipulateType): Dayjs { + return dayjs(date).add(value, unit) +} + +/** + * 日期减 + */ +export function subtractTime(date: ConfigType, value: number, unit: ManipulateType): Dayjs { + return dayjs(date).subtract(value, unit) +} + +/** + * 获取两个日期之间的差值 + */ +export function diff(date1: ConfigType, date2: ConfigType, unit: OpUnitType = 'day'): number { + return dayjs(date1).diff(dayjs(date2), unit) +} + +/** + * 判断日期是否在某个日期之前 + */ +export function isBefore(date1: ConfigType, date2: ConfigType): boolean { + return dayjs(date1).isBefore(dayjs(date2)) +} + +/** + * 判断日期是否在某个日期之后 + */ +export function isAfter(date1: ConfigType, date2: ConfigType): boolean { + return dayjs(date1).isAfter(dayjs(date2)) +} + +/** + * 判断两个日期是否相同 + */ +export function isSame(date1: ConfigType, date2: ConfigType, unit: OpUnitType = 'day'): boolean { + return dayjs(date1).isSame(dayjs(date2), unit) +} + +/** + * 获取当前时间戳 + */ +export function now(): number { + return dayjs().valueOf() +} + +/** + * 解析日期 + */ +export function parseDate(date: ConfigType): Dayjs { + return dayjs(date) +} + +/** + * 导出 dayjs 实例供高级用法 + */ +export { dayjs } diff --git a/packages/core/src/utils/helpers.ts b/packages/core/src/utils/helpers.ts new file mode 100644 index 0000000..80a1601 --- /dev/null +++ b/packages/core/src/utils/helpers.ts @@ -0,0 +1,79 @@ +/** + * 通用辅助函数 + */ + +const hexList: string[] = [] +for (let i = 0; i < 256; i++) { + hexList[i] = (i + 0x100).toString(16).substr(1) +} + +/** + * 高性能 UUID 生成器 + * 优先使用 crypto.randomUUID,降级使用 LUT 算法 + */ +export function generateUUID(): string { + if (typeof crypto !== 'undefined' && crypto.randomUUID) { + return crypto.randomUUID() + } + + const d0 = Math.random() * 0xFFFFFFFF | 0 + const d1 = Math.random() * 0xFFFFFFFF | 0 + const d2 = Math.random() * 0xFFFFFFFF | 0 + const d3 = Math.random() * 0xFFFFFFFF | 0 + + return `${hexList[d0 & 0xFF]}${hexList[d0 >> 8 & 0xFF]}${hexList[d0 >> 16 & 0xFF]}${hexList[d0 >> 24 & 0xFF]}-${ + hexList[d1 & 0xFF]}${hexList[d1 >> 8 & 0xFF]}-${ + hexList[d1 >> 16 & 0x0F | 0x40]}${hexList[d1 >> 24 & 0xFF]}-${ + hexList[d2 & 0x3F | 0x80]}${hexList[d2 >> 8 & 0xFF]}-${ + hexList[d2 >> 16 & 0xFF]}${hexList[d2 >> 24 & 0xFF]}${hexList[d3 & 0xFF]}${hexList[d3 >> 8 & 0xFF]}${hexList[d3 >> 16 & 0xFF]}${hexList[d3 >> 24 & 0xFF]}` +} + +export async function delayTimer(delay = 0) { + return new Promise((resolve) => { + const timer = setTimeout(() => { + resolve(true) + clearTimeout(timer) + }, delay) + }) +} + +function safeJsonClone(value: T): T { + const seen = new WeakSet() + const serialized = JSON.stringify(value, (_key, current) => { + if (typeof current === 'function' || typeof current === 'symbol') { + return undefined + } + if (typeof current === 'bigint') { + return Number(current) + } + if (typeof window !== 'undefined' && current === window) { + return undefined + } + if (typeof Node !== 'undefined' && current instanceof Node) { + return undefined + } + if (current && typeof current === 'object') { + if (seen.has(current)) { + return undefined + } + seen.add(current) + } + return current + }) + + if (serialized === undefined) { + return value + } + return JSON.parse(serialized) as T +} + +// 统一深拷贝:优先使用浏览器原生 structuredClone +export function cloneData(value: T): T { + if (typeof structuredClone === 'function') { + try { + return structuredClone(value) + } + catch {} + } + return safeJsonClone(value) +} diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts new file mode 100644 index 0000000..827c6ef --- /dev/null +++ b/packages/core/src/utils/index.ts @@ -0,0 +1,8 @@ +/** + * 工具函数统一导出 + */ + +export * from './auth' +export * from './canvas' +export * from './date' +export * from './helpers' diff --git a/packages/core/src/views/canvases/components/card.vue b/packages/core/src/views/canvases/components/card.vue new file mode 100644 index 0000000..a2e9b51 --- /dev/null +++ b/packages/core/src/views/canvases/components/card.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/packages/core/src/views/canvases/components/form.vue b/packages/core/src/views/canvases/components/form.vue new file mode 100644 index 0000000..c9051c3 --- /dev/null +++ b/packages/core/src/views/canvases/components/form.vue @@ -0,0 +1,111 @@ + + + diff --git a/packages/core/src/views/canvases/components/index.ts b/packages/core/src/views/canvases/components/index.ts new file mode 100644 index 0000000..dedd002 --- /dev/null +++ b/packages/core/src/views/canvases/components/index.ts @@ -0,0 +1,4 @@ +export { default as CanvasCard } from './card.vue' + +export { default as CanvasForm } from './form.vue' +export type { CanvasFormData } from './form.vue' diff --git a/packages/core/src/views/canvases/list.vue b/packages/core/src/views/canvases/list.vue new file mode 100644 index 0000000..424a2f2 --- /dev/null +++ b/packages/core/src/views/canvases/list.vue @@ -0,0 +1,362 @@ + + + + + diff --git a/packages/core/src/views/editor.vue b/packages/core/src/views/editor.vue new file mode 100644 index 0000000..19adc49 --- /dev/null +++ b/packages/core/src/views/editor.vue @@ -0,0 +1,51 @@ + + + diff --git a/packages/core/src/views/preview.vue b/packages/core/src/views/preview.vue new file mode 100644 index 0000000..4c3ca39 --- /dev/null +++ b/packages/core/src/views/preview.vue @@ -0,0 +1,274 @@ + + + + + diff --git a/packages/core/src/views/runtime.vue b/packages/core/src/views/runtime.vue new file mode 100644 index 0000000..9494ed0 --- /dev/null +++ b/packages/core/src/views/runtime.vue @@ -0,0 +1,955 @@ + + + + + + + diff --git a/packages/core/src/views/stand-alone/components/section-header.vue b/packages/core/src/views/stand-alone/components/section-header.vue new file mode 100644 index 0000000..2a41c89 --- /dev/null +++ b/packages/core/src/views/stand-alone/components/section-header.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/packages/core/src/views/stand-alone/components/settings/page-header.vue b/packages/core/src/views/stand-alone/components/settings/page-header.vue new file mode 100644 index 0000000..c2baa93 --- /dev/null +++ b/packages/core/src/views/stand-alone/components/settings/page-header.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/packages/core/src/views/stand-alone/components/settings/summary-card.vue b/packages/core/src/views/stand-alone/components/settings/summary-card.vue new file mode 100644 index 0000000..83e5cc2 --- /dev/null +++ b/packages/core/src/views/stand-alone/components/settings/summary-card.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/packages/core/src/views/stand-alone/components/types.ts b/packages/core/src/views/stand-alone/components/types.ts new file mode 100644 index 0000000..6c1de3f --- /dev/null +++ b/packages/core/src/views/stand-alone/components/types.ts @@ -0,0 +1,25 @@ +export type QuickActionKey = 'create' | 'import' | 'open' | 'settings' | 'canvases' + +export interface QuickAction { + key: QuickActionKey + title: string + description: string + icon: string + tone: 'primary' | 'neutral' +} + +export interface ProjectCard { + id: string + name: string + description: string + updatedAt: string + status: string + statusType: 'success' | 'warning' | 'info' + thumbnail: string +} + +export interface WorkflowStep { + no: string + title: string + description: string +} diff --git a/packages/core/src/views/stand-alone/components/welcome/quick-actions.vue b/packages/core/src/views/stand-alone/components/welcome/quick-actions.vue new file mode 100644 index 0000000..04141ff --- /dev/null +++ b/packages/core/src/views/stand-alone/components/welcome/quick-actions.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/packages/core/src/views/stand-alone/components/welcome/recent-projects.vue b/packages/core/src/views/stand-alone/components/welcome/recent-projects.vue new file mode 100644 index 0000000..f65f0a7 --- /dev/null +++ b/packages/core/src/views/stand-alone/components/welcome/recent-projects.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/packages/core/src/views/stand-alone/components/welcome/workflow-steps.vue b/packages/core/src/views/stand-alone/components/welcome/workflow-steps.vue new file mode 100644 index 0000000..7454ba8 --- /dev/null +++ b/packages/core/src/views/stand-alone/components/welcome/workflow-steps.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/packages/core/src/views/stand-alone/settings.vue b/packages/core/src/views/stand-alone/settings.vue new file mode 100644 index 0000000..17f9de9 --- /dev/null +++ b/packages/core/src/views/stand-alone/settings.vue @@ -0,0 +1,312 @@ + + + + + diff --git a/packages/core/src/views/stand-alone/welcome.vue b/packages/core/src/views/stand-alone/welcome.vue new file mode 100644 index 0000000..b216890 --- /dev/null +++ b/packages/core/src/views/stand-alone/welcome.vue @@ -0,0 +1,485 @@ + + + + + diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..4a8dff2 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + "@cslab-dcs/bridge": ["../bridge/src"], + "@cslab-dcs/request": ["../request/src"], + "@cslab-dcs/schema": ["../schema/src"] + }, + "types": ["element-plus/global"] + }, + "include": [ + "src/**/*", + "vite.shared.ts", + "env.d.ts", + "../bridge/src/**/*", + "../request/src/**/*", + "../schema/src/**/*", + "../utils/src/**/*" + ], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/core/unocss.config.ts b/packages/core/unocss.config.ts new file mode 100644 index 0000000..db42b18 --- /dev/null +++ b/packages/core/unocss.config.ts @@ -0,0 +1,31 @@ +import { + defineConfig, + presetAttributify, + presetIcons, + presetTypography, + presetUno, + transformerDirectives, + transformerVariantGroup, +} from 'unocss' +import presetChinese from 'unocss-preset-chinese' +import presetEase from 'unocss-preset-ease' + +export default defineConfig({ + presets: [ + presetUno(), + presetAttributify(), + presetChinese(), + presetEase(), + presetTypography(), + presetIcons({ + scale: 1.2, + warn: true, + }), + ], + shortcuts: [ + ['flex-center', 'flex items-center justify-center'], + ['flex-between', 'flex items-center justify-between'], + ['flex-end', 'flex items-end justify-between'], + ], + transformers: [transformerDirectives(), transformerVariantGroup()], +}) diff --git a/packages/core/vite.shared.ts b/packages/core/vite.shared.ts new file mode 100644 index 0000000..81ca0c4 --- /dev/null +++ b/packages/core/vite.shared.ts @@ -0,0 +1,69 @@ +import type { UserConfig } from 'vite' +import { resolve } from 'node:path' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import Unocss from 'unocss/vite' +import AutoImport from 'unplugin-auto-import/vite' +import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' +import Components from 'unplugin-vue-components/vite' + +export function createSharedViteConfig(rootDir: string, envs: Record = {}): Partial { + const plugins = [ + vue(), + vueJsx(), + Unocss(), + AutoImport({ + resolvers: [ElementPlusResolver()], + dts: resolve(rootDir, 'src/auto-imports.d.ts'), + }), + Components({ + resolvers: [ElementPlusResolver()], + dts: resolve(rootDir, 'src/components.d.ts'), + }), + ] as unknown as UserConfig['plugins'] + + return { + // 统一从 packages/core 读取环境变量 + envDir: __dirname, + plugins, + resolve: { + alias: { + '@': resolve(rootDir, 'src'), + '@cslab-dcs/core': resolve(__dirname, './src'), + }, + }, + css: { + preprocessorOptions: { + scss: { + // 自定义 element-plus 主题或全局变量 + additionalData: `@use "@/assets/styles/variables.scss" as *;`, + api: 'modern-compiler', // sass-loader v15+ requirement + }, + }, + }, + build: { + rollupOptions: { + output: { + manualChunks: { + 'vue-vendor': ['vue', 'vue-router', 'pinia', '@vueuse/core'], + 'element-plus': ['element-plus'], + 'general-utils': ['dayjs', 'zod', 'mitt', 'ofetch'], + }, + }, + }, + }, + server: { + proxy: { + '/cslab-server': { + target: envs.VITE_API_BASE_URL, + changeOrigin: true, + }, + '/chemical-chaos': { + target: envs.VITE_WS_DOMAIN, + changeOrigin: true, + ws: true, + }, + }, + }, + } +} diff --git a/packages/schema/package.json b/packages/schema/package.json new file mode 100644 index 0000000..c728ea1 --- /dev/null +++ b/packages/schema/package.json @@ -0,0 +1,27 @@ +{ + "name": "@cslab-dcs/schema", + "type": "module", + "version": "1.0.0", + "private": true, + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts" + }, + "./*": { + "types": "./src/*.ts", + "import": "./src/*.ts" + } + }, + "main": "./src/index.ts", + "types": "./src/index.ts", + "scripts": { + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "zod": "^3.24.1" + }, + "devDependencies": { + "typescript": "^5.9.3" + } +} diff --git a/packages/schema/src/canvas/base.ts b/packages/schema/src/canvas/base.ts new file mode 100644 index 0000000..b3fa1c3 --- /dev/null +++ b/packages/schema/src/canvas/base.ts @@ -0,0 +1,164 @@ +/** + * 画布基础类型 Schema + * 定义位置、尺寸、样式等基础类型 + */ +import { z } from 'zod' + +// ============================================ +// 基础几何类型 +// ============================================ + +/** + * 位置 Schema + */ +export const positionSchema = z.object({ + x: z.number().describe('X 坐标'), + y: z.number().describe('Y 坐标'), +}) + +export type Position = z.infer + +/** + * 尺寸 Schema + */ +export const sizeSchema = z.object({ + width: z.number().positive('宽度必须为正数').describe('宽度'), + height: z.number().positive('高度必须为正数').describe('高度'), +}) + +export type Size = z.infer + +/** + * 边界框 Schema + */ +export const boundingBoxSchema = z.object({ + x: z.number(), + y: z.number(), + width: z.number().positive(), + height: z.number().positive(), +}) + +export type BoundingBox = z.infer + +// ============================================ +// 样式类型 +// ============================================ + +/** + * 边框样式 Schema + */ +export const borderStyleSchema = z.object({ + color: z.string().optional().describe('边框颜色'), + width: z.number().nonnegative().optional().describe('边框宽度'), + style: z.enum(['solid', 'dashed', 'dotted', 'none']).optional().describe('边框样式'), + radius: z.number().nonnegative().optional().describe('圆角半径'), +}) + +export type BorderStyle = z.infer + +/** + * 填充样式 Schema + */ +export const fillStyleSchema = z.object({ + color: z.string().optional().describe('填充颜色'), + opacity: z.number().min(0).max(1).optional().describe('不透明度'), + gradient: z + .object({ + type: z.enum(['linear', 'radial']), + colors: z.array(z.string()), + stops: z.array(z.number().min(0).max(1)).optional(), + angle: z.number().optional(), + }) + .optional() + .describe('渐变填充'), + image: z + .object({ + src: z.string().describe('图片数据(base64)'), + fit: z.enum(['cover', 'contain', 'fill']).default('cover').describe('缩放方式'), + opacity: z.number().min(0).max(1).default(1).describe('图片透明度'), + }) + .optional() + .describe('图片填充'), +}) + +export type FillStyle = z.infer + +/** + * 文本样式 Schema + */ +export const textStyleSchema = z.object({ + fontSize: z.number().positive().optional().describe('字号'), + fontWeight: z.union([z.number(), z.enum(['normal', 'bold', 'lighter', 'bolder'])]).optional().describe('字重'), + fontStyle: z.enum(['normal', 'italic']).optional().describe('字体样式'), + textDecoration: z.enum(['none', 'underline']).optional().describe('文本装饰'), + color: z.string().optional().describe('文本颜色'), + align: z.enum(['left', 'center', 'right']).optional().describe('水平对齐'), + verticalAlign: z.enum(['top', 'middle', 'bottom']).optional().describe('垂直对齐'), +}) + +export type TextStyle = z.infer + +/** + * 阴影样式 Schema + */ +export const shadowStyleSchema = z.object({ + color: z.string().optional().describe('阴影颜色'), + blur: z.number().nonnegative().optional().describe('模糊半径'), + offsetX: z.number().optional().describe('X 偏移'), + offsetY: z.number().optional().describe('Y 偏移'), + opacity: z.number().min(0).max(1).optional().describe('阴影透明度'), + inset: z.boolean().optional().describe('是否内阴影'), + enabled: z.boolean().optional().describe('是否启用'), +}) + +export type ShadowStyle = z.infer + +/** + * 文本阴影样式 Schema + */ +export const textShadowStyleSchema = z.object({ + color: z.string().optional().describe('文本阴影颜色'), + blur: z.number().nonnegative().optional().describe('模糊半径'), + offsetX: z.number().optional().describe('X 偏移'), + offsetY: z.number().optional().describe('Y 偏移'), + enabled: z.boolean().optional().describe('是否启用'), +}) + +export type TextShadowStyle = z.infer + +/** + * 组件通用样式 Schema + */ +export const componentStyleSchema = z.object({ + fill: fillStyleSchema.optional(), + border: borderStyleSchema.optional(), + text: textStyleSchema.optional(), + shadow: shadowStyleSchema.optional(), + textShadow: textShadowStyleSchema.optional(), +}) + +export type ComponentStyle = z.infer + +// ============================================ +// 条件样式 +// ============================================ + +/** + * 条件样式覆盖属性(扁平结构,与 runtime 对齐) + */ +export const conditionalStyleOverrideSchema = z.record(z.string(), z.unknown()) + .describe('条件样式覆盖属性(扁平 key-value)') + +/** + * 条件样式规则 Schema + */ +export const conditionalStyleSchema = z.object({ + id: z.string().describe('规则 ID'), + name: z.string().optional().describe('规则名称'), + condition: z.string().describe('条件表达式'), + style: conditionalStyleOverrideSchema.describe('满足条件时应用的样式'), + priority: z.number().int().default(0).describe('优先级,数值越大优先级越高'), + enabled: z.boolean().optional().describe('是否启用该条件样式'), +}) + +export type ConditionalStyle = z.infer diff --git a/packages/schema/src/canvas/canvas.ts b/packages/schema/src/canvas/canvas.ts new file mode 100644 index 0000000..71602fb --- /dev/null +++ b/packages/schema/src/canvas/canvas.ts @@ -0,0 +1,165 @@ +/** + * 画布 Schema + * 定义画布主结构、背景、连接线等 + */ +import { z } from 'zod' +import { canvasComponentSchema } from './component' +import { variableSchema } from './variable' + +// 安全的 UUID 生成(兼容非安全上下文) +function generateUUID(): string { + if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { + return crypto.randomUUID() + } + // eslint-disable-next-line e18e/prefer-static-regex + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = Math.random() * 16 | 0 + return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16) + }) +} + +// ============================================ +// 背景配置 +// ============================================ + +/** + * 背景类型 + */ +export const backgroundTypeSchema = z.enum(['image', 'color', 'gradient', 'none']) + +export type BackgroundType = z.infer + +/** + * 背景配置 Schema + */ +export const backgroundSchema = z.object({ + type: backgroundTypeSchema.describe('背景类型'), + color: z.string().optional().describe('背景颜色'), + src: z.string().optional().describe('背景图片源'), + width: z.number().positive().optional().describe('背景逻辑宽度'), + height: z.number().positive().optional().describe('背景逻辑高度'), + lockAspectRatio: z.boolean().optional().describe('是否锁定宽高比'), + repeat: z.enum(['no-repeat', 'repeat', 'repeat-x', 'repeat-y']).optional().describe('重复方式'), + position: z.enum(['center', 'top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right']).optional().describe('位置'), + size: z.enum(['cover', 'contain', 'auto']).optional().describe('尺寸模式'), +}) + +export type Background = z.infer + +// ============================================ +// 视口配置 +// ============================================ + +/** + * 视口配置 Schema + */ +export const viewportSchema = z.object({ + x: z.number().default(0).describe('X 偏移'), + y: z.number().default(0).describe('Y 偏移'), + zoom: z.number().min(0.1).max(5).default(1).describe('缩放比例'), +}) + +export type Viewport = z.infer + +// ============================================ +// 画布主结构 +// ============================================ + +/** + * 画布类型 + */ +export const canvasTypeSchema = z.enum(['canvas', 'svg', 'custom']) + +export type CanvasType = z.infer + +/** + * 画布主题 + */ +export const canvasThemeSchema = z.enum([ + 'industrial-dark', // 工业深蓝 + 'industrial-light', // 工业浅色 + 'classic', // 经典 + 'modern', // 现代 + 'custom', // 自定义 +]) + +export type CanvasTheme = z.infer + +/** + * 画布样式 Schema + */ +export const canvasStyleSchema = z.object({ + background: backgroundSchema.optional().describe('背景配置'), +}) + +export type CanvasStyle = z.infer + +/** + * 画布配置 Schema + */ +export const canvasConfigSchema = z.object({ + viewport: viewportSchema.optional().describe('视口配置'), + style: canvasStyleSchema.optional().describe('画布样式'), +}) + +export type CanvasConfig = z.infer + +/** + * 画布 Schema + */ +export const canvasSchema = z.object({ + id: z.string().uuid().describe('画布唯一标识'), + name: z.string().min(1, '画布名不能为空').describe('画布名称'), + type: canvasTypeSchema.default('canvas').describe('画布类型'), + version: z.string().regex(/^\d+\.\d+\.\d+$/, '版本号格式应为 x.x.x').describe('版本号'), + width: z.number().positive().default(1920).describe('画布宽度'), + height: z.number().positive().default(1080).describe('画布高度'), + theme: canvasThemeSchema.optional().describe('主题'), + config: canvasConfigSchema.optional().describe('画布配置'), + components: z.array(canvasComponentSchema).describe('组件列表'), + vars: z.array(variableSchema).optional().describe('变量列表'), +}) + +export type Canvas = z.infer + +// ============================================ +// 画布验证工具 +// ============================================ + +/** + * 验证画布数据 + */ +export function validateCanvas(data: unknown): Canvas { + return canvasSchema.parse(data) +} + +/** + * 安全验证画布数据 + */ +export function safeParseCanvas(data: unknown) { + return canvasSchema.safeParse(data) +} + +/** + * 创建空画布 + */ +export function createEmptyCanvas(options: { + id?: string + name?: string + width?: number + height?: number +} = {}): Canvas { + return { + id: options.id || generateUUID(), + name: options.name || '新建画布', + type: 'canvas', + version: '1.0.0', + width: options.width || 1920, + height: options.height || 1080, + config: { + viewport: { x: 0, y: 0, zoom: 1 }, + style: { background: { type: 'color', color: '#1a2634' } }, + }, + components: [], + } +} diff --git a/packages/schema/src/canvas/component.ts b/packages/schema/src/canvas/component.ts new file mode 100644 index 0000000..ccab12c --- /dev/null +++ b/packages/schema/src/canvas/component.ts @@ -0,0 +1,304 @@ +/** + * 组件类型 Schema + * 定义各种画布组件的数据结构 + */ +import { z } from 'zod' +import { + componentStyleSchema, + conditionalStyleSchema, + positionSchema, + sizeSchema, +} from './base' +import { componentBindingsSchema } from './variable' + +// ============================================ +// 组件类型枚举 +// ============================================ + +/** + * 组件类型 + */ +export const componentTypeSchema = z.enum([ + 'rect', + 'number', + 'text', + 'bar', + 'button', + 'pidController', + 'valveController', + 'canvasSwitcher', + 'custom', +]) + +export type ComponentType = z.infer + +// ============================================ +// 组件基础 Schema +// ============================================ + +/** + * 组件事件触发类型 + */ +export const componentEventTriggerSchema = z.enum([ + 'click', + 'dblclick', +]) + +export type ComponentEventTrigger = z.infer + +/** + * 组件事件动作 Schema + */ +export const componentEventActionSchema = z.object({ + type: z.string().min(1).describe('动作类型'), + payload: z.record(z.unknown()).optional().describe('动作参数'), +}) + +export type ComponentEventAction = z.infer + +/** + * 组件事件 Schema + */ +export const componentEventSchema = z.object({ + id: z.string().min(1).describe('事件 ID'), + trigger: componentEventTriggerSchema.describe('触发类型'), + condition: z.string().optional().describe('触发条件'), + action: componentEventActionSchema.describe('动作'), +}) + +export type ComponentEvent = z.infer + +/** + * 组件基础属性 Schema + */ +export const componentBaseSchema = z.object({ + id: z.string().uuid().describe('组件唯一标识'), + type: componentTypeSchema.describe('组件类型'), + name: z.string().min(1, '组件名不能为空').describe('组件名称/位号'), + position: positionSchema.describe('组件位置'), + size: sizeSchema.describe('组件尺寸'), + zIndex: z.number().int().default(0).describe('层级'), + opacity: z.number().min(0).max(1).default(1).describe('不透明度'), + visible: z.boolean().default(true).describe('是否可见'), + tagNumber: z.string().optional().describe('设备位号'), + bindings: componentBindingsSchema.optional().describe('变量绑定'), + style: componentStyleSchema.optional().describe('样式'), + conditionalStyles: z.array(conditionalStyleSchema).optional().describe('条件样式'), + events: z.array(componentEventSchema).optional().describe('事件列表'), + metadata: z.record(z.unknown()).optional().describe('元数据'), +}) + +export type ComponentBase = z.infer + +// ============================================ +// 具体组件类型 +// ============================================ + +/** + * 矩形组件配置 + */ +export const rectComponentConfigSchema = z.object({ + fillColor: z.string().optional().describe('填充颜色'), +}) + +export type RectComponentConfig = z.infer + +/** + * 矩形组件 Schema + */ +export const rectComponentSchema = componentBaseSchema.extend({ + type: z.literal('rect'), + config: rectComponentConfigSchema.optional(), +}) + +export type RectComponent = z.infer + +/** + * 数字组件配置 + */ +export const numberComponentConfigSchema = z.object({ + fillColor: z.string().optional().describe('填充颜色'), + textColor: z.string().optional().describe('文字颜色'), + decimals: z.number().int().min(0).max(10).default(2).describe('小数位数'), + prefix: z.string().optional().describe('前缀'), + suffix: z.string().optional().describe('后缀/单位'), + thousandsSeparator: z.boolean().default(false).describe('千分位分隔'), + showTrend: z.boolean().default(false).describe('显示趋势箭头'), +}) + +export type NumberComponentConfig = z.infer + +/** + * 数字组件 Schema + */ +export const numberComponentSchema = componentBaseSchema.extend({ + type: z.literal('number'), + config: numberComponentConfigSchema.optional(), +}) + +export type NumberComponent = z.infer + +/** + * 文本组件配置 + */ +export const textComponentConfigSchema = z.object({ + fillColor: z.string().optional().describe('填充颜色'), + textColor: z.string().optional().describe('文字颜色'), + content: z.string().default('').describe('静态文本内容'), + isDynamic: z.boolean().default(false).describe('是否动态文本'), + expression: z.string().optional().describe('动态表达式'), +}) + +export type TextComponentConfig = z.infer + +/** + * 文本组件 Schema + */ +export const textComponentSchema = componentBaseSchema.extend({ + type: z.literal('text'), + config: textComponentConfigSchema.optional(), +}) + +export type TextComponent = z.infer + +/** + * 棒图组件配置 + */ +export const barComponentConfigSchema = z.object({ + value: z.number().optional().describe('当前值'), + direction: z.enum(['horizontal', 'vertical']).default('vertical').describe('方向'), + min: z.number().default(0).describe('最小值'), + max: z.number().default(100).describe('最大值'), + showValue: z.boolean().default(true).describe('显示数值'), + foregroundColor: z.string().default('#00ff00').describe('前景色(填充条颜色)'), + valueColor: z.string().default('#ffffff').describe('数值文字颜色'), + valueFontSize: z.number().positive().default(14).describe('数值字号'), + valueFontWeight: z.union([z.number(), z.enum(['normal', 'bold'])]).default(600).describe('数值字重'), + valueContent: z.string().optional().describe('数值显示内容(为空时显示百分比)'), + colors: z.array(z.object({ + threshold: z.number(), + color: z.string(), + })).optional().describe('阈值颜色(覆盖前景色)'), +}) + +export type BarComponentConfig = z.infer + +/** + * 棒图组件 Schema + */ +export const barComponentSchema = componentBaseSchema.extend({ + type: z.literal('bar'), + config: barComponentConfigSchema.optional(), +}) + +export type BarComponent = z.infer + +/** + * 按钮组件配置 + */ +export const buttonComponentConfigSchema = z.object({ + label: z.string().default('按钮').describe('按钮文字'), + buttonType: z.enum(['trigger', 'toggle', 'navigate']).default('trigger').describe('按钮类型'), + targetMethod: z.string().optional().describe('触发的方法名'), + targetCanvas: z.string().optional().describe('导航目标画布 ID'), + confirmRequired: z.boolean().default(false).describe('是否需要确认'), + confirmMessage: z.string().optional().describe('确认提示文字'), +}) + +export type ButtonComponentConfig = z.infer + +/** + * 按钮组件 Schema + */ +export const buttonComponentSchema = componentBaseSchema.extend({ + type: z.literal('button'), + config: buttonComponentConfigSchema.optional(), +}) + +export type ButtonComponent = z.infer + +/** + * 控制仪表组件配置 + */ +export const pidControllerComponentConfigSchema = z.object({ + headerColor: z.string().default('#0055ff').describe('标题栏背景色'), + labelColor: z.string().default('#00cc00').describe('字段标签颜色'), + valueColor: z.string().default('#ffffff').describe('数值颜色'), + bgColor: z.string().default('#1a1a2e').describe('面板背景色'), + decimalPlaces: z.number().int().min(0).max(6).default(2).describe('PV/SV 小数位数'), + opDecimalPlaces: z.number().int().min(0).max(6).default(2).describe('OP 小数位数'), + description: z.string().default('').describe('设备描述'), +}) + +export type PidControllerComponentConfig = z.infer + +export const pidControllerComponentSchema = componentBaseSchema.extend({ + type: z.literal('pidController'), + config: pidControllerComponentConfigSchema.optional(), +}) + +export type PidControllerComponent = z.infer + +/** + * 阀门控制器组件配置 + */ +export const valveControllerComponentConfigSchema = z.object({ + bgColor: z.string().default('transparent').describe('背景色'), + textColor: z.string().default('#ffffff').describe('文字颜色'), + decimalPlaces: z.number().int().min(0).max(6).default(2).describe('OP 小数位数'), + description: z.string().default('').describe('设备描述'), +}) + +export type ValveControllerComponentConfig = z.infer + +export const valveControllerComponentSchema = componentBaseSchema.extend({ + type: z.literal('valveController'), + config: valveControllerComponentConfigSchema.optional(), +}) + +export type ValveControllerComponent = z.infer + +/** + * 切页按钮组件配置 + */ +export const canvasSwitcherComponentConfigSchema = z.object({ + items: z.array(z.object({ + canvasId: z.string().describe('目标画布 ID'), + })).default([]).describe('切换项列表'), + activeColor: z.string().default('#E1E1E1').describe('激活项背景色'), + inactiveColor: z.string().default('#C8C8C8').describe('未激活项背景色'), + activeTextColor: z.string().default('#333333').describe('激活项文字颜色'), + inactiveTextColor: z.string().default('#333333').describe('未激活项文字颜色'), +}) + +export type CanvasSwitcherComponentConfig = z.infer + +export const canvasSwitcherComponentSchema = componentBaseSchema.extend({ + type: z.literal('canvasSwitcher'), + config: canvasSwitcherComponentConfigSchema.optional(), +}) + +export type CanvasSwitcherComponent = z.infer + +// ============================================ +// 联合类型 +// ============================================ + +/** + * 画布组件联合类型 Schema + */ +export const canvasComponentSchema: z.ZodType = z.lazy(() => + z.discriminatedUnion('type', [ + rectComponentSchema, + numberComponentSchema, + textComponentSchema, + barComponentSchema, + buttonComponentSchema, + pidControllerComponentSchema, + valveControllerComponentSchema, + canvasSwitcherComponentSchema, + componentBaseSchema.extend({ type: z.literal('custom') }), + ]), +) as z.ZodType + +export type CanvasComponent = z.infer diff --git a/packages/schema/src/canvas/index.ts b/packages/schema/src/canvas/index.ts new file mode 100644 index 0000000..28f83b1 --- /dev/null +++ b/packages/schema/src/canvas/index.ts @@ -0,0 +1,18 @@ +/** + * 画布 Schema 统一导出 + */ + +// 基础类型 +export * from './base' + +// 画布主结构 +export * from './canvas' + +// 组件类型 +export * from './component' + +// 图层数据模型 +export * from './layer' + +// 变量绑定 +export * from './variable' diff --git a/packages/schema/src/canvas/layer.ts b/packages/schema/src/canvas/layer.ts new file mode 100644 index 0000000..b1e1da9 --- /dev/null +++ b/packages/schema/src/canvas/layer.ts @@ -0,0 +1,72 @@ +import type { ComponentEvent, ComponentType } from './component' +/** + * 图层数据模型 + * 编辑器运行时使用的图层类型定义 + */ +import type { BindingExpression } from './variable' + +/** + * 图层绑定定义(BindingExpression 的别名,保持向后兼容) + */ +export type LayerBindingDefinition = BindingExpression + +/** + * 图层绑定值:支持简单字符串绑定、复杂绑定、或多条规则 + */ +export type LayerBindingValue = string | LayerBindingDefinition | Array + +/** + * 图层 + */ +export interface Layer { + id: string + type: ComponentType + x: number + y: number + width: number + height: number + config?: Record + style?: Record + bindings?: Record + tagNumber?: string + conditionalStyles?: Array<{ + id: string + condition: string + style: Record + priority?: number + enabled?: boolean + }> + events?: ComponentEvent[] +} + +/** + * 图层分组 + */ +export interface LayerGroup { + groupId: string + groupName: string + layerIds: string[] + layers: Layer[] + x: number + y: number + width: number + height: number +} + +/** + * 画布视图状态 + */ +export interface CanvasView { + canvasWidth: number + canvasHeight: number + imageWidth: number + imageHeight: number + imageNaturalWidth: number + imageNaturalHeight: number + backgroundLockAspectRatio: boolean + backgroundAverageColor: string | null + offsetX: number + offsetY: number + scale: number + background: string +} diff --git a/packages/schema/src/canvas/variable.ts b/packages/schema/src/canvas/variable.ts new file mode 100644 index 0000000..bf5436f --- /dev/null +++ b/packages/schema/src/canvas/variable.ts @@ -0,0 +1,88 @@ +/** + * 变量绑定 Schema + * 定义变量和绑定相关类型 + */ +import { z } from 'zod' + +// ============================================ +// 变量类型 +// ============================================ + +/** + * 变量值类型 + */ +export const variableValueTypeSchema = z.enum([ + 'analog', // 模拟量(数值) + 'digital', // 数字量(布尔) + 'string', // 字符串 + 'enum', // 枚举 +]) + +export type VariableValueType = z.infer + +/** + * 变量定义 Schema + */ +export const variableSchema = z.object({ + name: z.string().min(1, '变量名不能为空').describe('变量名'), + type: variableValueTypeSchema.describe('变量类型'), + description: z.string().optional().describe('变量描述'), + unit: z.string().optional().describe('单位'), + source: z.enum(['dynamic_project', 'manual', 'calculated']).optional().describe('数据来源'), + sourceId: z.string().optional().describe('来源 ID'), + defaultValue: z.unknown().optional().describe('默认值'), +}) + +export type Variable = z.infer + +// ============================================ +// 变量绑定 +// ============================================ + +/** + * 绑定表达式 Schema + * 支持直接绑定变量名或计算表达式 + */ +export const bindingExpressionSchema = z.object({ + id: z.string().optional().describe('绑定规则 ID'), + type: z.enum(['variable', 'expression']).describe('绑定类型'), + value: z.string().describe('变量名或表达式'), + variables: z.array(z.string()).optional().describe('表达式中引用的变量列表'), + priority: z.number().optional().describe('权重,数值越大优先级越高'), + enabled: z.boolean().optional().describe('是否启用该绑定规则'), +}) + +export type BindingExpression = z.infer + +/** + * 组件变量绑定 Schema + */ +export const componentBindingsSchema = z.record( + z.string(), // 绑定属性名 + z.union([ + z.string(), // 简单绑定:直接使用变量名 + bindingExpressionSchema, // 复杂绑定:表达式 + z.array(z.union([ + z.string(), + bindingExpressionSchema, + ])).describe('同一属性的多条绑定规则'), + ]), +) + +export type ComponentBindings = z.infer + +// ============================================ +// 变量值(运行时) +// ============================================ + +/** + * 变量快照 Schema + */ +export const variableSnapshotSchema = z.object({ + name: z.string(), + value: z.unknown(), + quality: z.enum(['good', 'bad', 'uncertain']).optional(), + timestamp: z.string().datetime().optional(), +}) + +export type VariableSnapshot = z.infer diff --git a/packages/schema/src/dcs.ts b/packages/schema/src/dcs.ts new file mode 100644 index 0000000..5472efc --- /dev/null +++ b/packages/schema/src/dcs.ts @@ -0,0 +1,99 @@ +/** + * DCS 配置相关类型和验证器 + */ +import { z } from 'zod' + +/** + * DCS 配置项类型 + */ +export type DCSValueType = 'string' | 'number' | 'boolean' | 'array' | 'object' + +/** + * DCS 配置项 + */ +export interface DCSConfigItem { + key: string + value: unknown + type: DCSValueType + description?: string + required?: boolean + children?: DCSConfigItem[] +} + +/** + * DCS 配置文件 + */ +export interface DCSConfigFile { + name: string + path: string + version: string + description?: string + items: DCSConfigItem[] + createdAt: string + updatedAt: string +} + +/** + * DCS 项目 + */ +export interface DCSProject { + id: string + name: string + description?: string + configs: DCSConfigFile[] + createdAt: string + updatedAt: string +} + +/** + * DCS 配置项验证器 + */ +export const dcsConfigItemSchema: z.ZodType = z.lazy(() => + z.object({ + key: z.string().min(1, '配置键不能为空'), + value: z.unknown(), + type: z.enum(['string', 'number', 'boolean', 'array', 'object']), + description: z.string().optional(), + required: z.boolean().optional(), + children: z.array(dcsConfigItemSchema).optional(), + }), +) as z.ZodType + +/** + * DCS 配置文件验证器 + */ +export const dcsConfigFileSchema = z.object({ + name: z.string().min(1, '文件名不能为空'), + path: z.string().min(1, '文件路径不能为空'), + version: z.string().regex(/^\d+\.\d+\.\d+$/, '版本号格式应为 x.x.x'), + description: z.string().optional(), + items: z.array(dcsConfigItemSchema), + createdAt: z.string().datetime(), + updatedAt: z.string().datetime(), +}) + +/** + * DCS 项目验证器 + */ +export const dcsProjectSchema = z.object({ + id: z.string().uuid(), + name: z.string().min(1, '项目名不能为空'), + description: z.string().optional(), + configs: z.array(dcsConfigFileSchema), + createdAt: z.string().datetime(), + updatedAt: z.string().datetime(), +}) + +/** + * 验证 DCS 配置 + */ +export function validateDCSConfig(data: unknown): DCSConfigFile { + return dcsConfigFileSchema.parse(data) +} + +/** + * 安全验证 DCS 配置 + */ +export function safeParseDCSConfig(data: unknown) { + return dcsConfigFileSchema.safeParse(data) +} diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts new file mode 100644 index 0000000..87eb7a4 --- /dev/null +++ b/packages/schema/src/index.ts @@ -0,0 +1,9 @@ +/** + * @cslab-dcs/schema + * 数据模型和验证器统一导出 + */ + +export * from './canvas' +export * from './dcs' +export * from './types' +export * from './validators' diff --git a/packages/schema/src/types.ts b/packages/schema/src/types.ts new file mode 100644 index 0000000..11c1725 --- /dev/null +++ b/packages/schema/src/types.ts @@ -0,0 +1,81 @@ +/** + * 通用类型定义 + */ + +/** + * API 响应结构 + */ +export interface ApiResponse { + code: number + message: string + data: T + success: boolean +} + +/** + * 分页请求参数 + */ +export interface PaginationParams { + page: number + pageSize: number +} + +/** + * 分页响应结构 + */ +export interface PaginatedData { + list: T[] + total: number + page: number + pageSize: number + totalPages: number +} + +/** + * 树形结构节点 + */ +export interface TreeNode { + id: string + label: string + children?: TreeNode[] + data?: T + disabled?: boolean + isLeaf?: boolean +} + +/** + * 键值对 + */ +export interface KeyValue { + key: string + value: T +} + +/** + * 选项结构 + */ +export interface SelectOption { + label: string + value: T + disabled?: boolean +} + +/** + * 用户信息 + */ +export interface UserInfo { + id: string + username: string + nickname?: string + avatar?: string + email?: string +} + +/** + * 应用状态 + */ +export interface AppState { + theme: 'light' | 'dark' | 'system' + language: string + sidebarCollapsed: boolean +} diff --git a/packages/schema/src/validators.ts b/packages/schema/src/validators.ts new file mode 100644 index 0000000..e0f2486 --- /dev/null +++ b/packages/schema/src/validators.ts @@ -0,0 +1,57 @@ +/** + * Zod 验证器 + */ +import { z } from 'zod' + +/** + * 通用验证规则 + */ +export const requiredString = z.string().min(1, '此字段为必填项') +export const optionalString = z.string().optional() +export const email = z.string().email('请输入有效的邮箱地址') +export const phone = z.string().regex(/^1[3-9]\d{9}$/, '请输入有效的手机号') +export const url = z.string().url('请输入有效的 URL') +export const positiveNumber = z.number().positive('请输入正数') +export const nonNegativeNumber = z.number().nonnegative('请输入非负数') + +/** + * API 响应验证 + */ +export const apiResponseSchema = z.object({ + code: z.number(), + message: z.string(), + data: z.unknown(), + success: z.boolean(), +}) + +/** + * 分页参数验证 + */ +export const paginationSchema = z.object({ + page: z.number().int().min(1).default(1), + pageSize: z.number().int().min(1).max(100).default(20), +}) + +/** + * 用户信息验证 + */ +export const userInfoSchema = z.object({ + id: z.string(), + username: z.string().min(2, '用户名至少2个字符'), + nickname: z.string().optional(), + avatar: z.string().url().optional(), + email: z.string().email().optional(), +}) + +/** + * 创建可选字段的 schema + */ +export function createPartialSchema(schema: z.ZodObject) { + return schema.partial() +} + +/** + * 重新导出 zod + */ +export { z } +export type { ZodError, ZodSchema, ZodType } from 'zod' diff --git a/packages/schema/tsconfig.json b/packages/schema/tsconfig.json new file mode 100644 index 0000000..4cdf33b --- /dev/null +++ b/packages/schema/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..9745283 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,28 @@ +import process from 'node:process' +import { defineConfig, devices } from '@playwright/test' + +export default defineConfig({ + testDir: './tests/e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: 'http://localhost:5173', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + webServer: { + command: 'pnpm dev:web', + url: 'http://localhost:5173', + reuseExistingServer: !process.env.CI, + timeout: 30000, + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..c78f2ed --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,9372 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@antfu/eslint-config': + specifier: ^7.2.0 + version: 7.7.0(@typescript-eslint/rule-tester@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3))(@typescript-eslint/utils@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.29)(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.0(@types/node@22.19.15)(happy-dom@20.8.4)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2))) + '@playwright/test': + specifier: ^1.58.2 + version: 1.58.2 + '@types/node': + specifier: ^22.19.7 + version: 22.19.15 + '@vitest/coverage-v8': + specifier: ^4.1.0 + version: 4.1.0(vitest@4.1.0(@types/node@22.19.15)(happy-dom@20.8.4)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2))) + '@vue/test-utils': + specifier: ^2.4.6 + version: 2.4.6 + eslint: + specifier: ^9.39.2 + version: 9.39.4(jiti@2.6.1) + happy-dom: + specifier: ^20.8.4 + version: 20.8.4 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vitest: + specifier: ^4.1.0 + version: 4.1.0(@types/node@22.19.15)(happy-dom@20.8.4)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2)) + + apps/electron: + dependencies: + '@cslab-dcs/bridge': + specifier: workspace:* + version: link:../../packages/bridge + '@cslab-dcs/core': + specifier: workspace:* + version: link:../../packages/core + '@electron-toolkit/preload': + specifier: 2.0.0 + version: 2.0.0(electron@22.3.27) + '@electron-toolkit/utils': + specifier: 2.0.0 + version: 2.0.0(electron@22.3.27) + electron-updater: + specifier: 6.3.9 + version: 6.3.9 + devDependencies: + '@vitejs/plugin-vue': + specifier: 6.0.1 + version: 6.0.1(vite@5.4.11(@types/node@22.19.15)(sass@1.97.3))(vue@3.5.13(typescript@5.9.3)) + '@vitejs/plugin-vue-jsx': + specifier: 4.1.1 + version: 4.1.1(vite@5.4.11(@types/node@22.19.15)(sass@1.97.3))(vue@3.5.13(typescript@5.9.3)) + electron: + specifier: 22.3.27 + version: 22.3.27 + electron-builder: + specifier: 25.1.8 + version: 25.1.8(electron-builder-squirrel-windows@25.1.8) + electron-vite: + specifier: 2.3.0 + version: 2.3.0(vite@5.4.11(@types/node@22.19.15)(sass@1.97.3)) + typescript: + specifier: 5.9.3 + version: 5.9.3 + vite: + specifier: 5.4.11 + version: 5.4.11(@types/node@22.19.15)(sass@1.97.3) + vue: + specifier: 3.5.13 + version: 3.5.13(typescript@5.9.3) + vue-tsc: + specifier: 2.2.0 + version: 2.2.0(typescript@5.9.3) + + apps/tauri: + dependencies: + '@cslab-dcs/bridge': + specifier: workspace:* + version: link:../../packages/bridge + '@cslab-dcs/core': + specifier: workspace:* + version: link:../../packages/core + '@tauri-apps/api': + specifier: 2.10.1 + version: 2.10.1 + '@tauri-apps/plugin-dialog': + specifier: ^2.2.0 + version: 2.6.0 + '@tauri-apps/plugin-fs': + specifier: ^2.2.0 + version: 2.4.5 + '@tauri-apps/plugin-opener': + specifier: ^2.2.5 + version: 2.5.3 + vue: + specifier: ^3.5.13 + version: 3.5.13(typescript@5.9.3) + devDependencies: + '@tauri-apps/cli': + specifier: 2.10.0 + version: 2.10.0 + '@vitejs/plugin-vue': + specifier: ^6.0.1 + version: 6.0.1(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3)) + '@vitejs/plugin-vue-jsx': + specifier: ^4.1.1 + version: 4.1.1(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3)) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite: + specifier: ^6.0.3 + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2) + vue-tsc: + specifier: ^2.2.0 + version: 2.2.0(typescript@5.9.3) + + apps/web: + dependencies: + '@cslab-dcs/bridge': + specifier: workspace:* + version: link:../../packages/bridge + '@cslab-dcs/core': + specifier: workspace:* + version: link:../../packages/core + vue: + specifier: ^3.5.13 + version: 3.5.13(typescript@5.9.3) + devDependencies: + '@vitejs/plugin-vue': + specifier: ^6.0.1 + version: 6.0.1(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3)) + '@vitejs/plugin-vue-jsx': + specifier: ^4.1.1 + version: 4.1.1(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3)) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite: + specifier: ^6.0.3 + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2) + vue-tsc: + specifier: ^2.2.0 + version: 2.2.0(typescript@5.9.3) + + packages/bridge: + dependencies: + '@tauri-apps/api': + specifier: 2.10.1 + version: 2.10.1 + '@tauri-apps/plugin-dialog': + specifier: ^2.2.0 + version: 2.6.0 + '@tauri-apps/plugin-fs': + specifier: ^2.2.0 + version: 2.4.5 + '@tauri-apps/plugin-opener': + specifier: ^2.2.5 + version: 2.5.3 + '@tauri-apps/plugin-os': + specifier: ^2.3.2 + version: 2.3.2 + devDependencies: + electron: + specifier: ^33.2.1 + version: 33.4.11 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + packages/core: + dependencies: + '@cslab-dcs/bridge': + specifier: workspace:* + version: link:../bridge + '@cslab-dcs/schema': + specifier: workspace:* + version: link:../schema + '@fortawesome/fontawesome-free': + specifier: ^7.1.0 + version: 7.2.0 + '@vueuse/core': + specifier: ^12.0.0 + version: 12.8.2(typescript@5.9.3) + dayjs: + specifier: ^1.11.19 + version: 1.11.19 + element-plus: + specifier: ^2.9.0 + version: 2.13.5(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)) + es-toolkit: + specifier: ^1.44.0 + version: 1.45.1 + mitt: + specifier: ^3.0.1 + version: 3.0.1 + ofetch: + specifier: ^1.5.1 + version: 1.5.1 + pinia: + specifier: ^3.0.0 + version: 3.0.4(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)) + vue: + specifier: ^3.5.13 + version: 3.5.13(typescript@5.9.3) + vue-router: + specifier: ^4.5.0 + version: 4.6.4(vue@3.5.13(typescript@5.9.3)) + zod: + specifier: ^3.24.1 + version: 3.25.76 + devDependencies: + '@unocss/preset-attributify': + specifier: ^66.6.0 + version: 66.6.6 + '@vitejs/plugin-vue': + specifier: ^6.0.1 + version: 6.0.1(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3)) + '@vitejs/plugin-vue-jsx': + specifier: ^4.1.1 + version: 4.1.1(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3)) + sass: + specifier: ^1.83.0 + version: 1.97.3 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + unocss: + specifier: ^66.6.0 + version: 66.6.6(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2)) + unocss-preset-chinese: + specifier: ^0.3.3 + version: 0.3.3(unocss@66.6.6(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2))) + unocss-preset-ease: + specifier: ^0.0.4 + version: 0.0.4(unocss@66.6.6(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2))) + unplugin-auto-import: + specifier: ^19.0.0 + version: 19.3.0(@vueuse/core@12.8.2(typescript@5.9.3)) + unplugin-vue-components: + specifier: ^28.0.0 + version: 28.8.0(@babel/parser@7.29.0)(vue@3.5.13(typescript@5.9.3)) + vite: + specifier: ^6.0.3 + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2) + vue-tsc: + specifier: ^2.2.0 + version: 2.2.0(typescript@5.9.3) + + packages/schema: + dependencies: + zod: + specifier: ^3.24.1 + version: 3.25.76 + devDependencies: + typescript: + specifier: ^5.9.3 + version: 5.9.3 + +packages: + + 7zip-bin@5.2.0: + resolution: {integrity: sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==} + + '@antfu/eslint-config@7.7.0': + resolution: {integrity: sha512-lkxb84o8z4v1+me51XlrHHF6zvOZfvTu6Y11t6h6v17JSMl9yoNHwC0Sqp/NfMTHie/LGgjyXOupXpQCXxfs1Q==} + hasBin: true + peerDependencies: + '@angular-eslint/eslint-plugin': ^21.1.0 + '@angular-eslint/eslint-plugin-template': ^21.1.0 + '@angular-eslint/template-parser': ^21.1.0 + '@eslint-react/eslint-plugin': ^2.11.0 + '@next/eslint-plugin-next': '>=15.0.0' + '@prettier/plugin-xml': ^3.4.1 + '@unocss/eslint-plugin': '>=0.50.0' + astro-eslint-parser: ^1.0.2 + eslint: ^9.10.0 || ^10.0.0 + eslint-plugin-astro: ^1.2.0 + eslint-plugin-format: '>=0.1.0' + eslint-plugin-jsx-a11y: '>=6.10.2' + eslint-plugin-react-hooks: ^7.0.0 + eslint-plugin-react-refresh: ^0.5.0 + eslint-plugin-solid: ^0.14.3 + eslint-plugin-svelte: '>=2.35.1' + eslint-plugin-vuejs-accessibility: ^2.4.1 + prettier-plugin-astro: ^0.14.0 + prettier-plugin-slidev: ^1.0.5 + svelte-eslint-parser: '>=0.37.0' + peerDependenciesMeta: + '@angular-eslint/eslint-plugin': + optional: true + '@angular-eslint/eslint-plugin-template': + optional: true + '@angular-eslint/template-parser': + optional: true + '@eslint-react/eslint-plugin': + optional: true + '@next/eslint-plugin-next': + optional: true + '@prettier/plugin-xml': + optional: true + '@unocss/eslint-plugin': + optional: true + astro-eslint-parser: + optional: true + eslint-plugin-astro: + optional: true + eslint-plugin-format: + optional: true + eslint-plugin-jsx-a11y: + optional: true + eslint-plugin-react-hooks: + optional: true + eslint-plugin-react-refresh: + optional: true + eslint-plugin-solid: + optional: true + eslint-plugin-svelte: + optional: true + eslint-plugin-vuejs-accessibility: + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-slidev: + optional: true + svelte-eslint-parser: + optional: true + + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.28.6': + resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.28.6': + resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-arrow-functions@7.27.1': + resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.6': + resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + + '@clack/core@1.1.0': + resolution: {integrity: sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA==} + + '@clack/prompts@1.1.0': + resolution: {integrity: sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g==} + + '@ctrl/tinycolor@4.2.0': + resolution: {integrity: sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==} + engines: {node: '>=14'} + + '@develar/schema-utils@2.6.5': + resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==} + engines: {node: '>= 8.9.0'} + + '@e18e/eslint-plugin@0.2.0': + resolution: {integrity: sha512-mXgODVwhuDjTJ+UT+XSvmMmCidtGKfrV5nMIv1UtpWex2pYLsIM3RSpT8HWIMAebS9qANbXPKlSX4BE7ZvuCgA==} + peerDependencies: + eslint: ^9.0.0 || ^10.0.0 + oxlint: ^1.41.0 + peerDependenciesMeta: + eslint: + optional: true + oxlint: + optional: true + + '@electron-toolkit/preload@2.0.0': + resolution: {integrity: sha512-zpZDzbqJTZQC5d4LRs2EKruKWnqah+T75s+niBYFemYLtiW5TTZcWi3Q8UxHqnwTudDMuWJb233aaS2yjx3Xiw==} + peerDependencies: + electron: '>=13.0.0' + + '@electron-toolkit/utils@2.0.0': + resolution: {integrity: sha512-taE/vvFOpoK5jyjUJTmMCjyGC2ODEObku4uvEt3PxoGPy25abGZcp9nbbbqBPqnfBjh88XSaPtnLRZNuOpHnnA==} + peerDependencies: + electron: '>=13.0.0' + + '@electron/asar@3.4.1': + resolution: {integrity: sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==} + engines: {node: '>=10.12.0'} + hasBin: true + + '@electron/get@2.0.3': + resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==} + engines: {node: '>=12'} + + '@electron/notarize@2.5.0': + resolution: {integrity: sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==} + engines: {node: '>= 10.0.0'} + + '@electron/osx-sign@1.3.1': + resolution: {integrity: sha512-BAfviURMHpmb1Yb50YbCxnOY0wfwaLXH5KJ4+80zS0gUkzDX3ec23naTlEqKsN+PwYn+a1cCzM7BJ4Wcd3sGzw==} + engines: {node: '>=12.0.0'} + hasBin: true + + '@electron/rebuild@3.6.1': + resolution: {integrity: sha512-f6596ZHpEq/YskUd8emYvOUne89ij8mQgjYFA5ru25QwbrRO+t1SImofdDv7kKOuWCmVOuU5tvfkbgGxIl3E/w==} + engines: {node: '>=12.13.0'} + hasBin: true + + '@electron/universal@2.0.1': + resolution: {integrity: sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==} + engines: {node: '>=16.4'} + + '@element-plus/icons-vue@2.3.2': + resolution: {integrity: sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==} + peerDependencies: + vue: ^3.2.0 + + '@emnapi/core@1.8.1': + resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} + + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + + '@es-joy/jsdoccomment@0.84.0': + resolution: {integrity: sha512-0xew1CxOam0gV5OMjh2KjFQZsKL2bByX1+q4j3E73MpYIdyUxcZb/xQct9ccUb+ve5KGUYbCUxyPnYB7RbuP+w==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@es-joy/resolve.exports@1.2.0': + resolution: {integrity: sha512-Q9hjxWI5xBM+qW2enxfe8wDKdFWMfd0Z29k5ZJnuBqD/CasY5Zryj09aCA6owbGATWz+39p5uIdaHXpopOcG8g==} + engines: {node: '>=10'} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-plugin-eslint-comments@4.7.1': + resolution: {integrity: sha512-Ql2nJFwA8wUGpILYGOQaT1glPsmvEwE0d+a+l7AALLzQvInqdbXJdx7aSu0DpUX9dB1wMVBMhm99/++S3MdEtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/compat@2.0.3': + resolution: {integrity: sha512-SjIJhGigp8hmd1YGIBwh7Ovri7Kisl42GYFjrOyHhtfYGGoLW6teYi/5p8W50KSsawUPpuLOSmsq1bD0NGQLBw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^8.40 || 9 || 10 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.5.3': + resolution: {integrity: sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@1.1.1': + resolution: {integrity: sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/markdown@7.5.1': + resolution: {integrity: sha512-R8uZemG9dKTbru/DQRPblbJyXpObwKzo8rv1KYGGuPUPtjM4LXBYM9q5CIZAComzZupws3tWbDwam5AFpPLyJQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.6.1': + resolution: {integrity: sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@fortawesome/fontawesome-free@7.2.0': + resolution: {integrity: sha512-3DguDv/oUE+7vjMeTSOjCSG+KeawgVQOHrKRnvUuqYh1mfArrh7s+s8hXW3e4RerBA1+Wh+hBqf8sJNpqNrBWg==} + engines: {node: '>=6'} + + '@gar/promisify@1.1.3': + resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@3.1.0': + resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@malept/cross-spawn-promise@2.0.0': + resolution: {integrity: sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==} + engines: {node: '>= 12.13.0'} + + '@malept/flatpak-bundler@0.4.0': + resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==} + engines: {node: '>= 10.0.0'} + + '@napi-rs/wasm-runtime@1.1.1': + resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + + '@npmcli/fs@2.1.2': + resolution: {integrity: sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + '@npmcli/move-file@2.0.1': + resolution: {integrity: sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This functionality has been moved to @npmcli/fs + + '@one-ini/wasm@0.1.1': + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + + '@ota-meshi/ast-token-store@0.3.0': + resolution: {integrity: sha512-XRO0zi2NIUKq2lUk3T1ecFSld1fMWRKE6naRFGkgkdeosx7IslyUKNv5Dcb5PJTja9tHJoFu0v/7yEpAkrkrTg==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@oxc-parser/binding-android-arm-eabi@0.115.0': + resolution: {integrity: sha512-VoB2rhgoqgYf64d6Qs5emONQW8ASiTc0xp+aUE4JUhxjX+0pE3gblTYDO0upcN5vt9UlBNmUhAwfSifkfre7nw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.115.0': + resolution: {integrity: sha512-lWRX75u+gqfB4TF3pWCHuvhaeneAmRl2b2qNBcl4S6yJ0HtnT4VXOMEZrq747i4Zby1ZTxj6mtOe678Bg8gRLw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.115.0': + resolution: {integrity: sha512-ii/oOZjfGY1aszXTy29Z5DRyCEnBOrAXDVCvfdfXFQsOZlbbOa7NMHD7D+06YFe5qdxfmbWAYv4yn6QJi/0d2g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.115.0': + resolution: {integrity: sha512-R/sW/p8l77wglbjpMcF+h/3rWbp9zk1mRP3U14mxTYIC2k3m+aLBpXXgk2zksqf9qKk5mcc4GIYsuCn9l8TgDg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.115.0': + resolution: {integrity: sha512-CSJ5ldNm9wIGGkhaIJeGmxRMZbgxThRN+X1ufYQQUNi5jZDV/U3C2QDMywpP93fczNBj961hXtcUPO/oVGq4Pw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.115.0': + resolution: {integrity: sha512-uWFwssE5dHfQ8lH+ktrsD9JA49+Qa0gtxZHUs62z1e91NgGz6O7jefHGI6aygNyKNS45pnnBSDSP/zV977MsOQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.115.0': + resolution: {integrity: sha512-fZbqt8y/sKQ+v6bBCuv/mYYFoC0+fZI3mGDDEemmDOhT78+aUs2+4ZMdbd2btlXmnLaScl37r8IRbhnok5Ka9w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.115.0': + resolution: {integrity: sha512-1ej/MjuTY9tJEunU/hUPIFmgH5PqgMQoRjNOvOkibtJ3Zqlw/+Lc+HGHDNET8sjbgIkWzdhX+p4J96A5CPdbag==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-arm64-musl@0.115.0': + resolution: {integrity: sha512-HjsZbJPH9mMd4swJRywVMsDZsJX0hyKb1iNHo5ijRl5yhtbO3lj7ImSrrL1oZ1VEg0te4iKmDGGz/6YPLd1G8w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-ppc64-gnu@0.115.0': + resolution: {integrity: sha512-zhhePoBrd7kQx3oClX/W6NldsuCbuMqaN9rRsY+6/WoorAb4j490PG/FjqgAXscWp2uSW2WV9L+ksn0wHrvsrg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-gnu@0.115.0': + resolution: {integrity: sha512-t/IRojvUE9XrKu+/H1b8YINug+7Q6FLls5rsm2lxB5mnS8GN/eYAYrPgHkcg9/1SueRDSzGpDYu3lGWTObk1zw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-musl@0.115.0': + resolution: {integrity: sha512-79jBHSSh/YpQRAmvYoaCfpyToRbJ/HBrdB7hxK2ku2JMehjopTVo+xMJss/RV7/ZYqeezgjvKDQzapJbgcjVZA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-s390x-gnu@0.115.0': + resolution: {integrity: sha512-nA1TpxkhNTIOMMyiSSsa7XIVJVoOU/SsVrHIz3gHvWweB5PHCQfO7w+Lb2EP0lBWokv7HtA/KbF7aLDoXzmuMw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-gnu@0.115.0': + resolution: {integrity: sha512-9iVX789DoC3SaOOG+X6NcF/tVChgLp2vcHffzOC2/Z1JTPlz6bMG2ogvcW6/9s0BG2qvhNQImd+gbWYeQbOwVw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-musl@0.115.0': + resolution: {integrity: sha512-RmQmk+mjCB0nMNfEYhaCxwofLo1Z95ebHw1AGvRiWGCd4zhCNOyskgCbMogIcQzSB3SuEKWgkssyaiQYVAA4hQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-openharmony-arm64@0.115.0': + resolution: {integrity: sha512-viigraWWQhhDvX5aGq+wrQq58k00Xq3MHz/0R4AFMxGlZ8ogNonpEfNc73Q5Ly87Z6sU9BvxEdG0dnYTfVnmew==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.115.0': + resolution: {integrity: sha512-IzGCrMwXhpb4kTXy/8lnqqqwjI7eOvy+r9AhVw+hsr8t1ecBBEHprcNy0aKatFHN6hsX7UMHHQmBAQjVvL/p1A==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.115.0': + resolution: {integrity: sha512-/ym+Absk/TLFvbhh3se9XYuI1D7BrUVHw4RaG/2dmWKgBenrZHaJsgnRb7NJtaOyjEOLIPtULx1wDdVL0SX2eg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.115.0': + resolution: {integrity: sha512-AQSZjIR+b+Te7uaO/hGTMjT8/oxlYrvKrOTi4KTHF/O6osjHEatUQ3y6ZW2+8+lJxy20zIcGz6iQFmFq/qDKkg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.115.0': + resolution: {integrity: sha512-oxUl82N+fIO9jIaXPph8SPPHQXrA08BHokBBJW8ct9F/x6o6bZE6eUAhUtWajbtvFhL8UYcCWRMba+kww6MBlA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.115.0': + resolution: {integrity: sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==} + + '@parcel/watcher-android-arm64@2.5.6': + resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.6': + resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.6': + resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.6': + resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.6': + resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm-musl@2.5.6': + resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm64-musl@2.5.6': + resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-x64-glibc@2.5.6': + resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-x64-musl@2.5.6': + resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@parcel/watcher-win32-arm64@2.5.6': + resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.6': + resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.6': + resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.6': + resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} + engines: {node: '>= 10.0.0'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@playwright/test@1.58.2': + resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} + engines: {node: '>=18'} + hasBin: true + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@quansync/fs@1.0.0': + resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + + '@rolldown/pluginutils@1.0.0-beta.29': + resolution: {integrity: sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==} + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@sindresorhus/base62@1.0.0': + resolution: {integrity: sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==} + engines: {node: '>=18'} + + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@stylistic/eslint-plugin@5.10.0': + resolution: {integrity: sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^9.0.0 || ^10.0.0 + + '@sxzz/popperjs-es@2.11.8': + resolution: {integrity: sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==} + + '@szmarczak/http-timer@4.0.6': + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + + '@tauri-apps/api@2.10.1': + resolution: {integrity: sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==} + + '@tauri-apps/cli-darwin-arm64@2.10.0': + resolution: {integrity: sha512-avqHD4HRjrMamE/7R/kzJPcAJnZs0IIS+1nkDP5b+TNBn3py7N2aIo9LIpy+VQq0AkN8G5dDpZtOOBkmWt/zjA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tauri-apps/cli-darwin-x64@2.10.0': + resolution: {integrity: sha512-keDmlvJRStzVFjZTd0xYkBONLtgBC9eMTpmXnBXzsHuawV2q9PvDo2x6D5mhuoMVrJ9QWjgaPKBBCFks4dK71Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tauri-apps/cli-linux-arm-gnueabihf@2.10.0': + resolution: {integrity: sha512-e5u0VfLZsMAC9iHaOEANumgl6lfnJx0Dtjkd8IJpysZ8jp0tJ6wrIkto2OzQgzcYyRCKgX72aKE0PFgZputA8g==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tauri-apps/cli-linux-arm64-gnu@2.10.0': + resolution: {integrity: sha512-YrYYk2dfmBs5m+OIMCrb+JH/oo+4FtlpcrTCgiFYc7vcs6m3QDd1TTyWu0u01ewsCtK2kOdluhr/zKku+KP7HA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-arm64-musl@2.10.0': + resolution: {integrity: sha512-GUoPdVJmrJRIXFfW3Rkt+eGK9ygOdyISACZfC/bCSfOnGt8kNdQIQr5WRH9QUaTVFIwxMlQyV3m+yXYP+xhSVA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tauri-apps/cli-linux-riscv64-gnu@2.10.0': + resolution: {integrity: sha512-JO7s3TlSxshwsoKNCDkyvsx5gw2QAs/Y2GbR5UE2d5kkU138ATKoPOtxn8G1fFT1aDW4LH0rYAAfBpGkDyJJnw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-x64-gnu@2.10.0': + resolution: {integrity: sha512-Uvh4SUUp4A6DVRSMWjelww0GnZI3PlVy7VS+DRF5napKuIehVjGl9XD0uKoCoxwAQBLctvipyEK+pDXpJeoHng==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-x64-musl@2.10.0': + resolution: {integrity: sha512-AP0KRK6bJuTpQ8kMNWvhIpKUkQJfcPFeba7QshOQZjJ8wOS6emwTN4K5g/d3AbCMo0RRdnZWwu67MlmtJyxC1Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tauri-apps/cli-win32-arm64-msvc@2.10.0': + resolution: {integrity: sha512-97DXVU3dJystrq7W41IX+82JEorLNY+3+ECYxvXWqkq7DBN6FsA08x/EFGE8N/b0LTOui9X2dvpGGoeZKKV08g==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tauri-apps/cli-win32-ia32-msvc@2.10.0': + resolution: {integrity: sha512-EHyQ1iwrWy1CwMalEm9z2a6L5isQ121pe7FcA2xe4VWMJp+GHSDDGvbTv/OPdkt2Lyr7DAZBpZHM6nvlHXEc4A==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@tauri-apps/cli-win32-x64-msvc@2.10.0': + resolution: {integrity: sha512-NTpyQxkpzGmU6ceWBTY2xRIEaS0ZLbVx1HE1zTA3TY/pV3+cPoPPOs+7YScr4IMzXMtOw7tLw5LEXo5oIG3qaQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tauri-apps/cli@2.10.0': + resolution: {integrity: sha512-ZwT0T+7bw4+DPCSWzmviwq5XbXlM0cNoleDKOYPFYqcZqeKY31KlpoMW/MOON/tOFBPgi31a2v3w9gliqwL2+Q==} + engines: {node: '>= 10'} + hasBin: true + + '@tauri-apps/plugin-dialog@2.6.0': + resolution: {integrity: sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==} + + '@tauri-apps/plugin-fs@2.4.5': + resolution: {integrity: sha512-dVxWWGE6VrOxC7/jlhyE+ON/Cc2REJlM35R3PJX3UvFw2XwYhLGQVAIyrehenDdKjotipjYEVc4YjOl3qq90fA==} + + '@tauri-apps/plugin-opener@2.5.3': + resolution: {integrity: sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==} + + '@tauri-apps/plugin-os@2.3.2': + resolution: {integrity: sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A==} + + '@tootallnate/once@2.0.0': + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/cacheable-request@6.0.3': + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/fs-extra@9.0.13': + resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} + + '@types/http-cache-semantics@4.2.0': + resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/keyv@3.1.4': + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.24': + resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@16.18.126': + resolution: {integrity: sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==} + + '@types/node@20.19.37': + resolution: {integrity: sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==} + + '@types/node@22.19.15': + resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} + + '@types/plist@3.0.5': + resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==} + + '@types/responselike@1.0.3': + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/verror@1.10.11': + resolution: {integrity: sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==} + + '@types/web-bluetooth@0.0.20': + resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} + + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@types/whatwg-mimetype@3.0.2': + resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + + '@typescript-eslint/eslint-plugin@8.56.1': + resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.56.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.56.1': + resolution: {integrity: sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.56.1': + resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/rule-tester@8.56.1': + resolution: {integrity: sha512-EWuV5Vq1EFYJEOVcILyWPO35PjnT0c6tv99PCpD12PgfZae5/Jo+F17hGjsEs2Moe+Dy1J7KIr8y037cK8+/rQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + + '@typescript-eslint/scope-manager@8.56.1': + resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.56.1': + resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.56.1': + resolution: {integrity: sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.56.1': + resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.56.1': + resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.56.1': + resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.56.1': + resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@unocss/cli@66.6.6': + resolution: {integrity: sha512-78SY8j4hAVelK+vP/adsDGaSjEITasYLFECJLHWxUJSzK+G9UIc5wtL/u4jA+zKvwVkHcDvbkcO5K6wwwpAixg==} + engines: {node: '>=14'} + hasBin: true + + '@unocss/config@66.6.6': + resolution: {integrity: sha512-menlnkqAFX/4wR2aandY8hSqrt01JE+rOzvtQxWaBt8kf1du62b0sS72FE5Z40n6HlEsEbF91N9FCfhnzG6i6g==} + engines: {node: '>=14'} + + '@unocss/core@0.62.4': + resolution: {integrity: sha512-Cc+Vo6XlaQpyVejkJrrzzWtiK9pgMWzVVBpm9VCVtwZPUjD4GSc+g7VQCPXSsr7m03tmSuRySJx72QcASmauNQ==} + + '@unocss/core@66.6.6': + resolution: {integrity: sha512-Sbbx0ZQqmV8K2lg8E+z9MJzWb1MgRtJnvqzxDIrNuBjXasKhbcFt5wEMBtEZJOr63Z4ck0xThhZK53HmYT2jmg==} + + '@unocss/extractor-arbitrary-variants@0.62.4': + resolution: {integrity: sha512-e4hJfBMyFr6T6dYSTTjNv9CQwaU1CVEKxDlYP0GpfSgxsV58pguID9j1mt0/XZD6LvEDzwxj9RTRWKpUSWqp+Q==} + + '@unocss/extractor-arbitrary-variants@66.6.6': + resolution: {integrity: sha512-uMzekF2miZRUwSZGvy3yYQiBAcSAs9LiXK8e3NjldxEw8xcRDWgTErxgStRoBeAD6UyzDcg/Cvwtf2guMbtR+g==} + + '@unocss/inspector@66.6.6': + resolution: {integrity: sha512-CpXIsqHwxCXJtUjUz6S29diHCIA+EJ1u5WML/6m2YPI4ObgWAVKrExy09inSg2icS52lFkWWdWQSeqc9kl5W6Q==} + + '@unocss/preset-attributify@66.6.6': + resolution: {integrity: sha512-3H12UI1rBt60PQy+S4IEeFYWu1/WQFuc2yhJ5mu/RCvX5/qwlIGanBpuh+xzTPXU1fWBlZN68yyO9uWOQgTqZQ==} + + '@unocss/preset-icons@66.6.6': + resolution: {integrity: sha512-HfIEEqf3jyKexOB2Sux556n0NkPoUftb2H4+Cf7prJvKHopMkZ/OUkXjwvUlxt1e5UpAEaIa0A2Ir7+ApxXoGA==} + + '@unocss/preset-mini@0.62.4': + resolution: {integrity: sha512-1O+QpQFx7FT61aheAZEYemW5e4AGib8TFGm+rWLudKq2IBNnXHcS5xsq5QvqdC7rp9Dn3lnW5du6ijow5kCBuw==} + + '@unocss/preset-mini@66.6.6': + resolution: {integrity: sha512-k+/95PKMPOK57cJcSmz34VkIFem8BlujRRx6/L0Yusw7vLJMh98k0rPhC5s+NomZ/d9ZPgbNylskLhItJlak3w==} + + '@unocss/preset-tagify@66.6.6': + resolution: {integrity: sha512-KgBXYPYS0g4TVC3NLiIB78YIqUlvDLanz1EHIDo34rOTUfMgY8Uf5VuDJAzMu4Sc0LiwwBJbk6nIG9/Zm7ufWg==} + + '@unocss/preset-typography@66.6.6': + resolution: {integrity: sha512-SM1km5nqt15z4sTabfOobSC633I5Ol5nnme6JFTra4wiyCUNs+Cg31nJ6jnopWDUT4SEAXqfUH7jKSSoCnI6ZA==} + + '@unocss/preset-uno@66.6.6': + resolution: {integrity: sha512-40PcBDtlhW7QP7e/WOxC684IhN5T1dXvj1dgx9ZzK+8lEDGjcX7bN2noW4aSenzSrHymeSsMrL/0ltL4ED/5Zw==} + + '@unocss/preset-web-fonts@66.6.6': + resolution: {integrity: sha512-5ikwgrJB8VPzKd0bqgGNgYUGix90KFnVtKJPjWTP5qsv3+ZtZnea1rRbAFl8i2t52hg35msNBsQo+40IC3xB6A==} + + '@unocss/preset-wind3@66.6.6': + resolution: {integrity: sha512-rk6gPPIQ7z2DVucOqp7XZ4vGpKAuzBV1vtUDvDh5WscxzO/QlqaeTfTALk5YgGpmLaF4+ns6FrTgLjV+wHgHuQ==} + + '@unocss/preset-wind4@66.6.6': + resolution: {integrity: sha512-caTDM9rZSlp4tyPWWAnwMvQr2PXq53LsEYwd3N8zj0ou2hcsqptJvF+mFvyhvGF66x26wWJr/FwuUEhh7qycaw==} + + '@unocss/preset-wind@66.6.6': + resolution: {integrity: sha512-TMy3lZ35FP/4QqDHOLWZmV+RoOGWUDqnDEOTjOKI1CQARGta0ppUmq+IZMuI1ZJLuOa4OZ9V6SfnwMXwRLgXmw==} + + '@unocss/rule-utils@0.62.4': + resolution: {integrity: sha512-XUwLbLUzL+VSHCJNK5QBHC9RbFehumge1/XJmsRfmh0+oxgJoO1gvEvxi57gYEmdJdMRJHRJZ66se6+cB0Ymvw==} + engines: {node: '>=14'} + + '@unocss/rule-utils@66.6.6': + resolution: {integrity: sha512-krWtQKGshOaqQMuxeGq1NOA8NL35VdpYlmQEWOe39BY6TACT51bgQFu40MRfsAIMZZtoGS2YYTrnHojgR92omw==} + engines: {node: '>=14'} + + '@unocss/transformer-attributify-jsx@66.6.6': + resolution: {integrity: sha512-NnDchmN2EeFLy4lfVqDgNe9j1+w2RLL2L9zKECXs5g6rDVfeeEK6FNgxSq3XnPcKltjNCy1pF4MaDOROG7r8yA==} + + '@unocss/transformer-compile-class@66.6.6': + resolution: {integrity: sha512-KKssJxU8fZ9x84yznIirbtta2sB0LN/3lm0bp+Wl1298HITaNiVeG2n26iStQ3N7r240xRN2RarxncSVCMFwWw==} + + '@unocss/transformer-directives@66.6.6': + resolution: {integrity: sha512-CReFTcBfMtKkRvzIqxL20VptWt5C1Om27dwoKzyVFBXv0jzViWysbu0y0AQg3bsgD4cFqndFyAGyeL84j0nbKg==} + + '@unocss/transformer-variant-group@66.6.6': + resolution: {integrity: sha512-j4L/0Tw6AdMVB2dDnuBlDbevyL1/0CAk88a77VF/VjgEIBwB9VXsCCUsxz+2Dohcl7N2GMm7+kpaWA6qt2PSaA==} + + '@unocss/vite@66.6.6': + resolution: {integrity: sha512-DgG7KcUUMtoDhPOlFf2l4dR+66xZ23SdZvTYpikk5nZfLCzZd62vedutD7x0bTR6VpK2YRq39B+F+Z6TktNY/w==} + peerDependencies: + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0-0 + + '@vitejs/plugin-vue-jsx@4.1.1': + resolution: {integrity: sha512-uMJqv/7u1zz/9NbWAD3XdjaY20tKTf17XVfQ9zq4wY1BjsB/PjpJPMe2xiG39QpP4ZdhYNhm4Hvo66uJrykNLA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.0.0 + + '@vitejs/plugin-vue@6.0.1': + resolution: {integrity: sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + vue: ^3.2.25 + + '@vitest/coverage-v8@4.1.0': + resolution: {integrity: sha512-nDWulKeik2bL2Va/Wl4x7DLuTKAXa906iRFooIRPR+huHkcvp9QDkPQ2RJdmjOFrqOqvNfoSQLF68deE3xC3CQ==} + peerDependencies: + '@vitest/browser': 4.1.0 + vitest: 4.1.0 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/eslint-plugin@1.6.9': + resolution: {integrity: sha512-9WfPx1OwJ19QLCSRLkqVO7//1WcWnK3fE/3fJhKMAmDe8+9G4rB47xCNIIeCq3FdEzkIoLTfDlwDlPBaUTMhow==} + engines: {node: '>=18'} + peerDependencies: + eslint: '>=8.57.0' + typescript: '>=5.0.0' + vitest: '*' + peerDependenciesMeta: + typescript: + optional: true + vitest: + optional: true + + '@vitest/expect@4.1.0': + resolution: {integrity: sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==} + + '@vitest/mocker@4.1.0': + resolution: {integrity: sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.0': + resolution: {integrity: sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==} + + '@vitest/runner@4.1.0': + resolution: {integrity: sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==} + + '@vitest/snapshot@4.1.0': + resolution: {integrity: sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==} + + '@vitest/spy@4.1.0': + resolution: {integrity: sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==} + + '@vitest/utils@4.1.0': + resolution: {integrity: sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==} + + '@volar/language-core@2.4.28': + resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} + + '@volar/source-map@2.4.28': + resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==} + + '@volar/typescript@2.4.28': + resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} + + '@vue/babel-helper-vue-transform-on@1.5.0': + resolution: {integrity: sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==} + + '@vue/babel-plugin-jsx@1.5.0': + resolution: {integrity: sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + peerDependenciesMeta: + '@babel/core': + optional: true + + '@vue/babel-plugin-resolve-type@1.5.0': + resolution: {integrity: sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@vue/compiler-core@3.5.13': + resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} + + '@vue/compiler-core@3.5.29': + resolution: {integrity: sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==} + + '@vue/compiler-dom@3.5.13': + resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==} + + '@vue/compiler-dom@3.5.29': + resolution: {integrity: sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==} + + '@vue/compiler-sfc@3.5.13': + resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==} + + '@vue/compiler-sfc@3.5.29': + resolution: {integrity: sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==} + + '@vue/compiler-ssr@3.5.13': + resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==} + + '@vue/compiler-ssr@3.5.29': + resolution: {integrity: sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==} + + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/devtools-api@7.7.9': + resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==} + + '@vue/devtools-kit@7.7.9': + resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==} + + '@vue/devtools-shared@7.7.9': + resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==} + + '@vue/language-core@2.2.0': + resolution: {integrity: sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/reactivity@3.5.13': + resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==} + + '@vue/runtime-core@3.5.13': + resolution: {integrity: sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==} + + '@vue/runtime-dom@3.5.13': + resolution: {integrity: sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==} + + '@vue/server-renderer@3.5.13': + resolution: {integrity: sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==} + peerDependencies: + vue: 3.5.13 + + '@vue/shared@3.5.13': + resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} + + '@vue/shared@3.5.29': + resolution: {integrity: sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==} + + '@vue/test-utils@2.4.6': + resolution: {integrity: sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==} + + '@vueuse/core@12.0.0': + resolution: {integrity: sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==} + + '@vueuse/core@12.8.2': + resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==} + + '@vueuse/metadata@12.0.0': + resolution: {integrity: sha512-Yzimd1D3sjxTDOlF05HekU5aSGdKjxhuhRFHA7gDWLn57PRbBIh+SF5NmjhJ0WRgF3my7T8LBucyxdFJjIfRJQ==} + + '@vueuse/metadata@12.8.2': + resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==} + + '@vueuse/shared@12.0.0': + resolution: {integrity: sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==} + + '@vueuse/shared@12.8.2': + resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==} + + '@xmldom/xmldom@0.8.11': + resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} + engines: {node: '>=10.0.0'} + + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ajv-keywords@3.5.2: + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + + alien-signals@0.4.14: + resolution: {integrity: sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + app-builder-bin@5.0.0-alpha.10: + resolution: {integrity: sha512-Ev4jj3D7Bo+O0GPD2NMvJl+PGiBAfS7pUGawntBNpCbxtpncfUixqFj9z9Jme7V7s3LBGqsWZZP54fxBX3JKJw==} + + app-builder-lib@25.1.8: + resolution: {integrity: sha512-pCqe7dfsQFBABC1jeKZXQWhGcCPF3rPCXDdfqVKjIeWBcXzyC1iOWZdfFhGl+S9MyE/k//DFmC6FzuGAUudNDg==} + engines: {node: '>=14.0.0'} + peerDependencies: + dmg-builder: 25.1.8 + electron-builder-squirrel-windows: 25.1.8 + + aproba@2.1.0: + resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==} + + archiver-utils@2.1.0: + resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} + engines: {node: '>= 6'} + + archiver-utils@3.0.4: + resolution: {integrity: sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==} + engines: {node: '>= 10'} + + archiver@5.3.2: + resolution: {integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==} + engines: {node: '>= 10'} + + are-docs-informative@0.0.2: + resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} + engines: {node: '>=14'} + + are-we-there-yet@3.0.1: + resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + assert-plus@1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-v8-to-istanbul@1.0.0: + resolution: {integrity: sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + async-exit-hook@2.0.1: + resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==} + engines: {node: '>=0.12.0'} + + async-validator@4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.10.0: + resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==} + engines: {node: '>=6.0.0'} + hasBin: true + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + bluebird-lst@1.0.9: + resolution: {integrity: sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==} + + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + brace-expansion@5.0.4: + resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + engines: {node: 18 || 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + builder-util-runtime@9.2.10: + resolution: {integrity: sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==} + engines: {node: '>=12.0.0'} + + builder-util@25.1.7: + resolution: {integrity: sha512-7jPjzBwEGRbwNcep0gGNpLXG9P94VA3CPAZQCzxkFXiV2GMQKlziMbY//rXPI7WKfhsvGgFXjTcXdBEwgXw9ww==} + + builtin-modules@5.0.0: + resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==} + engines: {node: '>=18.20'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + cac@7.0.0: + resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} + engines: {node: '>=20.19.0'} + + cacache@16.1.3: + resolution: {integrity: sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + cacheable-lookup@5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + + cacheable-request@7.0.4: + resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001777: + resolution: {integrity: sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + chromium-pickle-js@0.2.0: + resolution: {integrity: sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + + clean-regexp@1.0.0: + resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} + engines: {node: '>=4'} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-truncate@2.1.0: + resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} + engines: {node: '>=8'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + commander@5.1.0: + resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} + engines: {node: '>= 6'} + + comment-parser@1.4.5: + resolution: {integrity: sha512-aRDkn3uyIlCFfk5NUA+VdwMmMsh8JGhc4hapfV4yxymHGQ3BVskMQfoXGpCo5IoBuQ9tS5iiVKhCpTcB4pW4qw==} + engines: {node: '>= 12.0.0'} + + compare-version@0.1.2: + resolution: {integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==} + engines: {node: '>=0.10.0'} + + compress-commons@4.1.2: + resolution: {integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==} + engines: {node: '>= 10'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + + config-file-ts@0.2.8-rc1: + resolution: {integrity: sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + copy-anything@4.0.5: + resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} + engines: {node: '>=18'} + + core-js-compat@3.48.0: + resolution: {integrity: sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==} + + core-util-is@1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@4.0.3: + resolution: {integrity: sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==} + engines: {node: '>= 10'} + + crc@3.8.0: + resolution: {integrity: sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + dir-compare@4.2.0: + resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==} + + dmg-builder@25.1.8: + resolution: {integrity: sha512-NoXo6Liy2heSklTI5OIZbCgXC1RzrDQsZkeEwXhdOro3FT1VBOvbubvscdPnjVuQ4AMwwv61oaH96AbiYg9EnQ==} + + dmg-license@1.0.11: + resolution: {integrity: sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==} + engines: {node: '>=8'} + os: [darwin] + hasBin: true + + dotenv-expand@11.0.7: + resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} + engines: {node: '>=12'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + editorconfig@1.0.7: + resolution: {integrity: sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==} + engines: {node: '>=14'} + hasBin: true + + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + electron-builder-squirrel-windows@25.1.8: + resolution: {integrity: sha512-2ntkJ+9+0GFP6nAISiMabKt6eqBB0kX1QqHNWFWAXgi0VULKGisM46luRFpIBiU3u/TDmhZMM8tzvo2Abn3ayg==} + + electron-builder@25.1.8: + resolution: {integrity: sha512-poRgAtUHHOnlzZnc9PK4nzG53xh74wj2Jy7jkTrqZ0MWPoHGh1M2+C//hGeYdA+4K8w4yiVCNYoLXF7ySj2Wig==} + engines: {node: '>=14.0.0'} + hasBin: true + + electron-publish@25.1.7: + resolution: {integrity: sha512-+jbTkR9m39eDBMP4gfbqglDd6UvBC7RLh5Y0MhFSsc6UkGHj9Vj9TWobxevHYMMqmoujL11ZLjfPpMX+Pt6YEg==} + + electron-to-chromium@1.5.307: + resolution: {integrity: sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==} + + electron-updater@6.3.9: + resolution: {integrity: sha512-2PJNONi+iBidkoC5D1nzT9XqsE8Q1X28Fn6xRQhO3YX8qRRyJ3mkV4F1aQsuRnYPqq6Hw+E51y27W75WgDoofw==} + + electron-vite@2.3.0: + resolution: {integrity: sha512-lsN2FymgJlp4k6MrcsphGqZQ9fKRdJKasoaiwIrAewN1tapYI/KINLdfEL7n10LuF0pPSNf/IqjzZbB5VINctg==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@swc/core': ^1.0.0 + vite: ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + '@swc/core': + optional: true + + electron@22.3.27: + resolution: {integrity: sha512-7Rht21vHqj4ZFRnKuZdFqZFsvMBCmDqmjetiMqPtF+TmTBiGne1mnstVXOA/SRGhN2Qy5gY5bznJKpiqogjM8A==} + engines: {node: '>= 12.20.55'} + hasBin: true + + electron@33.4.11: + resolution: {integrity: sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg==} + engines: {node: '>= 12.20.55'} + hasBin: true + + element-plus@2.13.5: + resolution: {integrity: sha512-dmY24fhSREfZN/PuUt0YZigMso7wWzl+B5o+YKNN15kQIn/0hzamsPU+ebj9SES0IbUqsLX1wkrzYmzU8VrVOQ==} + peerDependencies: + vue: ^3.3.0 + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + enhanced-resolve@5.20.0: + resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==} + engines: {node: '>=10.13.0'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-toolkit@1.45.1: + resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==} + + es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + eslint-compat-utils@0.5.1: + resolution: {integrity: sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==} + engines: {node: '>=12'} + peerDependencies: + eslint: '>=6.0.0' + + eslint-config-flat-gitignore@2.2.1: + resolution: {integrity: sha512-wA5EqN0era7/7Gt5Botlsfin/UNY0etJSEeBgbUlFLFrBi47rAN//+39fI7fpYcl8RENutlFtvp/zRa/M/pZNg==} + peerDependencies: + eslint: ^9.5.0 || ^10.0.0 + + eslint-flat-config-utils@3.0.2: + resolution: {integrity: sha512-mPvevWSDQFwgABvyCurwIu6ZdKxGI5NW22/BGDwA1T49NO6bXuxbV9VfJK/tkQoNyPogT6Yu1d57iM0jnZVWmg==} + + eslint-json-compat-utils@0.2.2: + resolution: {integrity: sha512-KcTUifi8VSSHkrOY0FzB7smuTZRU9T2nCrcCy6k2b+Q77+uylBQVIxN4baVCIWvWJEpud+IsrYgco4JJ6io05g==} + engines: {node: '>=12'} + peerDependencies: + '@eslint/json': '*' + eslint: '*' + jsonc-eslint-parser: ^2.4.0 || ^3.0.0 + peerDependenciesMeta: + '@eslint/json': + optional: true + + eslint-merge-processors@2.0.0: + resolution: {integrity: sha512-sUuhSf3IrJdGooquEUB5TNpGNpBoQccbnaLHsb1XkBLUPPqCNivCpY05ZcpCOiV9uHwO2yxXEWVczVclzMxYlA==} + peerDependencies: + eslint: '*' + + eslint-plugin-antfu@3.2.2: + resolution: {integrity: sha512-Qzixht2Dmd/pMbb5EnKqw2V8TiWHbotPlsORO8a+IzCLFwE0RxK8a9k4DCTFPzBwyxJzH+0m2Mn8IUGeGQkyUw==} + peerDependencies: + eslint: '*' + + eslint-plugin-command@3.5.2: + resolution: {integrity: sha512-PA59QAkQDwvcCMEt5lYLJLI3zDGVKJeC4id/pcRY2XdRYhSGW7iyYT1VC1N3bmpuvu6Qb/9QptiS3GJMjeGTJg==} + peerDependencies: + '@typescript-eslint/rule-tester': '*' + '@typescript-eslint/typescript-estree': '*' + '@typescript-eslint/utils': '*' + eslint: '*' + + eslint-plugin-depend@1.5.0: + resolution: {integrity: sha512-i3UeLYmclf1Icp35+6W7CR4Bp2PIpDgBuf/mpmXK5UeLkZlvYJ21VuQKKHHAIBKRTPivPGX/gZl5JGno1o9Y0A==} + peerDependencies: + eslint: '>=8.40.0' + + eslint-plugin-es-x@7.8.0: + resolution: {integrity: sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '>=8' + + eslint-plugin-import-lite@0.5.2: + resolution: {integrity: sha512-XvfdWOC5dSLEI9krIPRlNmKSI2ViIE9pVylzfV9fCq0ZpDaNeUk6o0wZv0OzN83QdadgXp1NsY0qjLINxwYCsw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=9.0.0' + + eslint-plugin-jsdoc@62.7.1: + resolution: {integrity: sha512-4Zvx99Q7d1uggYBUX/AIjvoyqXhluGbbKrRmG8SQTLprPFg6fa293tVJH1o1GQwNe3lUydd8ZHzn37OaSncgSQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-jsonc@3.1.1: + resolution: {integrity: sha512-7TSQO8ZyvOuXWb0sYke3KUSh0DJA4/QviKfuzD3/Cy3XDjtrIrTWQbjb7j/Yy2l/DgwuM+lCS2c/jqJifv5jhg==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: '>=9.38.0' + + eslint-plugin-n@17.24.0: + resolution: {integrity: sha512-/gC7/KAYmfNnPNOb3eu8vw+TdVnV0zhdQwexsw6FLXbhzroVj20vRn2qL8lDWDGnAQ2J8DhdfvXxX9EoxvERvw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=8.23.0' + + eslint-plugin-no-only-tests@3.3.0: + resolution: {integrity: sha512-brcKcxGnISN2CcVhXJ/kEQlNa0MEfGRtwKtWA16SkqXHKitaKIMrfemJKLKX1YqDU5C/5JY3PvZXd5jEW04e0Q==} + engines: {node: '>=5.0.0'} + + eslint-plugin-perfectionist@5.6.0: + resolution: {integrity: sha512-pxrLrfRp5wl1Vol1fAEa/G5yTXxefTPJjz07qC7a8iWFXcOZNuWBItMQ2OtTzfQIvMq6bMyYcrzc3Wz++na55Q==} + engines: {node: ^20.0.0 || >=22.0.0} + peerDependencies: + eslint: ^8.45.0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-pnpm@1.6.0: + resolution: {integrity: sha512-dxmt9r3zvPaft6IugS4i0k16xag3fTbOvm/road5uV9Y8qUCQT0xzheSh3gMlYAlC6vXRpfArBDsTZ7H7JKCbg==} + peerDependencies: + eslint: ^9.0.0 || ^10.0.0 + + eslint-plugin-regexp@3.0.0: + resolution: {integrity: sha512-iW7hgAV8NOG6E2dz+VeKpq67YLQ9jaajOKYpoOSic2/q8y9BMdXBKkSR9gcMtbqEhNQzdW41E3wWzvhp8ExYwQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: '>=9.38.0' + + eslint-plugin-toml@1.3.1: + resolution: {integrity: sha512-1l00fBP03HIt9IPV7ZxBi7x0y0NMdEZmakL1jBD6N/FoKBvfKxPw5S8XkmzBecOnFBTn5Z8sNJtL5vdf9cpRMQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: '>=9.38.0' + + eslint-plugin-unicorn@63.0.0: + resolution: {integrity: sha512-Iqecl9118uQEXYh7adylgEmGfkn5es3/mlQTLLkd4pXkIk9CTGrAbeUux+YljSa2ohXCBmQQ0+Ej1kZaFgcfkA==} + engines: {node: ^20.10.0 || >=21.0.0} + peerDependencies: + eslint: '>=9.38.0' + + eslint-plugin-unused-imports@4.4.1: + resolution: {integrity: sha512-oZGYUz1X3sRMGUB+0cZyK2VcvRX5lm/vB56PgNNcU+7ficUCKm66oZWKUubXWnOuPjQ8PvmXtCViXBMONPe7tQ==} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0 + eslint: ^10.0.0 || ^9.0.0 || ^8.0.0 + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + + eslint-plugin-vue@10.8.0: + resolution: {integrity: sha512-f1J/tcbnrpgC8suPN5AtdJ5MQjuXbSU9pGRSSYAuF3SHoiYCOdEX6O22pLaRyLHXvDcOe+O5ENgc1owQ587agA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@stylistic/eslint-plugin': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 + '@typescript-eslint/parser': ^7.0.0 || ^8.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + vue-eslint-parser: ^10.0.0 + peerDependenciesMeta: + '@stylistic/eslint-plugin': + optional: true + '@typescript-eslint/parser': + optional: true + + eslint-plugin-yml@3.3.1: + resolution: {integrity: sha512-isntsZchaTqDMNNkD+CakrgA/pdUoJ45USWBKpuqfAW1MCuw731xX/vrXfoJFZU3tTFr24nCbDYmDfT2+g4QtQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + peerDependencies: + eslint: '>=9.38.0' + + eslint-processor-vue-blocks@2.0.0: + resolution: {integrity: sha512-u4W0CJwGoWY3bjXAuFpc/b6eK3NQEI8MoeW7ritKj3G3z/WtHrKjkqf+wk8mPEy5rlMGS+k6AZYOw2XBoN/02Q==} + peerDependencies: + '@vue/compiler-sfc': ^3.3.0 + eslint: '>=9.0.0' + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + exponential-backoff@3.1.3: + resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} + + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + extsprintf@1.4.1: + resolution: {integrity: sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==} + engines: {'0': node >=0.6.0} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fault@2.0.1: + resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + filelist@1.0.6: + resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up-simple@1.0.1: + resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} + engines: {node: '>=18'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.0: + resolution: {integrity: sha512-kC6Bb+ooptOIvWj5B63EQWkF0FEnNjV2ZNkLMLZRDDduIiWeFF4iKnslwhiWxjAdbg4NzTNo6h0qLuvFrcx+Sw==} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + format@0.2.2: + resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} + engines: {node: '>=0.4.x'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@11.3.4: + resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} + engines: {node: '>=14.14'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gauge@4.0.4: + resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-tsconfig@4.13.6: + resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + deprecated: Glob versions prior to v9 are no longer supported + + global-agent@3.0.0: + resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} + engines: {node: '>=10.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.15.0: + resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} + engines: {node: '>=18'} + + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} + engines: {node: '>=18'} + + globals@17.4.0: + resolution: {integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + got@11.8.6: + resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} + engines: {node: '>=10.19.0'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + gzip-size@6.0.0: + resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} + engines: {node: '>=10'} + + happy-dom@20.8.4: + resolution: {integrity: sha512-GKhjq4OQCYB4VLFBzv8mmccUadwlAusOZOI7hC1D9xDIT5HhzkJK17c4el2f6R6C715P9xB4uiMxeKUa2nHMwQ==} + engines: {node: '>=20.0.0'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + + html-entities@2.6.0: + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + http2-wrapper@1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + iconv-corefoundation@1.1.7: + resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==} + engines: {node: ^8.11.2 || >=10} + os: [darwin] + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + immutable@5.1.5: + resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + indent-string@5.0.0: + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} + + infer-owner@1.0.4: + resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-builtin-module@5.0.0: + resolution: {integrity: sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==} + engines: {node: '>=18.20'} + + is-ci@3.0.1: + resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-lambda@1.0.1: + resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-what@5.5.0: + resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} + engines: {node: '>=18'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isbinaryfile@4.0.10: + resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==} + engines: {node: '>= 8.0.0'} + + isbinaryfile@5.0.7: + resolution: {integrity: sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==} + engines: {node: '>= 18.0.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jake@10.9.4: + resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} + engines: {node: '>=10'} + hasBin: true + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-beautify@1.15.4: + resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} + engines: {node: '>=14'} + hasBin: true + + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsdoc-type-pratt-parser@7.1.1: + resolution: {integrity: sha512-/2uqY7x6bsrpi3i9LVU6J89352C0rpMk0as8trXxCtvd4kPk1ke/Eyif6wqfSLvoNJqcDG9Vk4UsXgygzCt2xA==} + engines: {node: '>=20.0.0'} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-eslint-parser@3.1.0: + resolution: {integrity: sha512-75EA7EWZExL/j+MDKQrRbdzcRI2HOkRlmUw8fZJc1ioqFEOvBsq7Rt+A6yCxOt9w/TYNpkt52gC6nm/g5tFIng==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + lazy-val@1.0.5: + resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} + + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + local-pkg@1.1.2: + resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + engines: {node: '>=14'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash-es@4.17.23: + resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + + lodash-unified@1.0.3: + resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==} + peerDependencies: + '@types/lodash-es': '*' + lodash: '*' + lodash-es: '*' + + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.difference@4.5.0: + resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==} + + lodash.escaperegexp@4.1.2: + resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} + + lodash.flatten@4.4.0: + resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} + + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.union@4.6.0: + resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} + + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + magic-regexp@0.10.0: + resolution: {integrity: sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.5.2: + resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + make-fetch-happen@10.2.1: + resolution: {integrity: sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + matcher@3.0.0: + resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} + engines: {node: '>=10'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-frontmatter@2.0.1: + resolution: {integrity: sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-frontmatter@2.0.0: + resolution: {integrity: sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass-collect@1.0.2: + resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} + engines: {node: '>= 8'} + + minipass-fetch@2.1.2: + resolution: {integrity: sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + minipass-flush@1.0.5: + resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} + engines: {node: '>= 8'} + + minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + + minipass-sized@1.0.3: + resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + engines: {node: '>=8'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + mlly@1.8.1: + resolution: {integrity: sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==} + + module-replacements@2.11.0: + resolution: {integrity: sha512-j5sNQm3VCpQQ7nTqGeOZtoJtV3uKERgCBm9QRhmGRiXiqkf7iRFOkfxdJRZWLkqYY8PNf4cDQF/WfXUYLENrRA==} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + natural-orderby@5.0.0: + resolution: {integrity: sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg==} + engines: {node: '>=18'} + + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + + node-abi@3.87.0: + resolution: {integrity: sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==} + engines: {node: '>=10'} + + node-addon-api@1.7.2: + resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-api-version@0.2.1: + resolution: {integrity: sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==} + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-gyp@9.4.1: + resolution: {integrity: sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==} + engines: {node: ^12.13 || ^14.13 || >=16} + hasBin: true + + node-releases@2.0.36: + resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + + nopt@6.0.0: + resolution: {integrity: sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + hasBin: true + + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + + normalize-wheel-es@1.2.0: + resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==} + + npmlog@6.0.2: + resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + object-deep-merge@2.0.0: + resolution: {integrity: sha512-3DC3UMpeffLTHiuXSy/UG4NOIYTLlY9u3V82+djSCLYClWobZiS4ivYzpIUWrRY/nfsJ8cWsKyG3QfyLePmhvg==} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + ofetch@1.5.1: + resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + oxc-parser@0.115.0: + resolution: {integrity: sha512-2w7Xn3CbS/zwzSY82S5WLemrRu3CT57uF7Lx8llrE/2bul6iMTcJE4Rbls7GDNbLn3ttATI68PfOz2Pt3KZ2cQ==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxc-walker@0.7.0: + resolution: {integrity: sha512-54B4KUhrzbzc4sKvKwVYm7E2PgeROpGba0/2nlNZMqfDyca+yOor5IMb4WLGBatGDT0nkzYdYuzylg7n3YfB7A==} + peerDependencies: + oxc-parser: '>=0.98.0' + + p-cancelable@2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-gitignore@2.0.0: + resolution: {integrity: sha512-RmVuCHWsfu0QPNW+mraxh/xjQVw/lhUCUru8Zni3Ctq3AoMhpDTq0OVdKS6iesd6Kqb7viCV3isAL43dciOSog==} + engines: {node: '>=14'} + + parse-imports-exports@0.2.4: + resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==} + + parse-statements@1.0.11: + resolution: {integrity: sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pe-library@0.4.1: + resolution: {integrity: sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==} + engines: {node: '>=12', npm: '>=6'} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pinia@3.0.4: + resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==} + peerDependencies: + typescript: '>=4.5.0' + vue: ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + + playwright-core@1.58.2: + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.58.2: + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + engines: {node: '>=18'} + hasBin: true + + plist@3.1.0: + resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} + engines: {node: '>=10.4.0'} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + pnpm-workspace-yaml@1.6.0: + resolution: {integrity: sha512-uUy4dK3E11sp7nK+hnT7uAWfkBMe00KaUw8OG3NuNlYQoTk4sc9pcdIy1+XIP85v9Tvr02mK3JPaNNrP0QyRaw==} + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + promise-inflight@1.0.1: + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} + peerDependencies: + bluebird: '*' + peerDependenciesMeta: + bluebird: + optional: true + + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + quansync@1.0.0: + resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + + read-binary-file-arch@1.0.6: + resolution: {integrity: sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==} + hasBin: true + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + + refa@0.12.1: + resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + regexp-ast-analysis@0.7.1: + resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + regexp-tree@0.1.27: + resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} + hasBin: true + + regjsparser@0.13.0: + resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==} + hasBin: true + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resedit@1.7.2: + resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==} + engines: {node: '>=12', npm: '>=6'} + + reserved-identifiers@1.2.0: + resolution: {integrity: sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==} + engines: {node: '>=18'} + + resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + responselike@2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + roarr@2.15.4: + resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} + engines: {node: '>=8.0'} + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sanitize-filename@1.6.3: + resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==} + + sass@1.97.3: + resolution: {integrity: sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==} + engines: {node: '>=14.0.0'} + hasBin: true + + sax@1.5.0: + resolution: {integrity: sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==} + engines: {node: '>=11.0.0'} + + scslre@0.3.0: + resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} + engines: {node: ^14.0.0 || >=16.0.0} + + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + + semver-compare@1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + serialize-error@7.0.1: + resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} + engines: {node: '>=10'} + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slice-ansi@3.0.0: + resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} + engines: {node: '>=8'} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@7.0.0: + resolution: {integrity: sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==} + engines: {node: '>= 10'} + + socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@4.0.0: + resolution: {integrity: sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==} + + spdx-license-ids@3.0.23: + resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + + ssri@9.0.1: + resolution: {integrity: sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + stat-mode@1.0.0: + resolution: {integrity: sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==} + engines: {node: '>= 6'} + + std-env@4.0.0: + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-indent@4.1.1: + resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + sumchecker@3.0.1: + resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} + engines: {node: '>= 8.0'} + + superjson@2.2.6: + resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} + engines: {node: '>=16'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + synckit@0.11.12: + resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} + engines: {node: ^14.18.0 || >=16.0.0} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + + temp-file@3.4.0: + resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==} + + tiny-typed-emitter@2.1.0: + resolution: {integrity: sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tmp-promise@3.0.3: + resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} + + tmp@0.2.5: + resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} + engines: {node: '>=14.14'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + to-valid-identifier@1.0.0: + resolution: {integrity: sha512-41wJyvKep3yT2tyPqX/4blcfybknGB4D+oETKLs7Q76UiPqRpUJK3hr1nxelyYO0PHKVzJwlu0aCeEAsGI6rpw==} + engines: {node: '>=20'} + + toml-eslint-parser@1.0.3: + resolution: {integrity: sha512-A5F0cM6+mDleacLIEUkmfpkBbnHJFV1d2rprHU2MXNk7mlxHq2zGojA+SRvQD1RoMo9gqjZPWEaKG4v1BQ48lw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + truncate-utf8-bytes@1.0.2: + resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} + + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-declaration-location@1.0.7: + resolution: {integrity: sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA==} + peerDependencies: + typescript: '>=4.0.0' + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + + type-level-regexp@0.1.17: + resolution: {integrity: sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + + unconfig-core@7.5.0: + resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} + + unconfig@7.5.0: + resolution: {integrity: sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unimport@4.2.0: + resolution: {integrity: sha512-mYVtA0nmzrysnYnyb3ALMbByJ+Maosee2+WyE0puXl+Xm2bUwPorPaaeZt0ETfuroPOtG8jj1g/qeFZ6buFnag==} + engines: {node: '>=18.12.0'} + + unique-filename@2.0.1: + resolution: {integrity: sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + unique-slug@3.0.0: + resolution: {integrity: sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unocss-preset-chinese@0.3.3: + resolution: {integrity: sha512-t6AZ5HMb2pMwSuBp1ntVViKUwPufLWRELoptkAIQrK53j9CtGU3wGXGcpas8HQXaG5fzSpwmGRJagB+7bz1ZZw==} + peerDependencies: + '@unocss/nuxt': '*' + unocss: '*' + peerDependenciesMeta: + '@unocss/nuxt': + optional: true + unocss: + optional: true + + unocss-preset-ease@0.0.4: + resolution: {integrity: sha512-WCPcfDV93YHh8uQ3rxPY3/SbYAD7pff532qLFaIoKr7TrjzV4lVow8bTwqmyNhJGPhC470do5GOmB2oDOcjUcA==} + peerDependencies: + '@unocss/nuxt': '*' + unocss: '*' + peerDependenciesMeta: + '@unocss/nuxt': + optional: true + unocss: + optional: true + + unocss@66.6.6: + resolution: {integrity: sha512-PRKK945e2oZKHV664MA5Z9CDHbvY/V79IvTOUWKZ514jpl3UsJU3sS+skgxmKJSmwrWvXE5OVcmPthJrD/7vxg==} + engines: {node: '>=14'} + peerDependencies: + '@unocss/astro': 66.6.6 + '@unocss/postcss': 66.6.6 + '@unocss/webpack': 66.6.6 + peerDependenciesMeta: + '@unocss/astro': + optional: true + '@unocss/postcss': + optional: true + '@unocss/webpack': + optional: true + + unplugin-auto-import@19.3.0: + resolution: {integrity: sha512-iIi0u4Gq2uGkAOGqlPJOAMI8vocvjh1clGTfSK4SOrJKrt+tirrixo/FjgBwXQNNdS7ofcr7OxzmOb/RjWxeEQ==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': ^3.2.2 + '@vueuse/core': '*' + peerDependenciesMeta: + '@nuxt/kit': + optional: true + '@vueuse/core': + optional: true + + unplugin-utils@0.2.5: + resolution: {integrity: sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg==} + engines: {node: '>=18.12.0'} + + unplugin-utils@0.3.1: + resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} + engines: {node: '>=20.19.0'} + + unplugin-vue-components@28.8.0: + resolution: {integrity: sha512-2Q6ZongpoQzuXDK0ZsVzMoshH0MWZQ1pzVL538G7oIDKRTVzHjppBDS8aB99SADGHN3lpGU7frraCG6yWNoL5Q==} + engines: {node: '>=14'} + peerDependencies: + '@babel/parser': ^7.15.8 + '@nuxt/kit': ^3.2.2 || ^4.0.0 + vue: 2 || 3 + peerDependenciesMeta: + '@babel/parser': + optional: true + '@nuxt/kit': + optional: true + + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + utf8-byte-length@1.0.5: + resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + verror@1.10.1: + resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} + engines: {node: '>=0.6.0'} + + vite@5.4.11: + resolution: {integrity: sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vite@6.4.1: + resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.0: + resolution: {integrity: sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.0 + '@vitest/browser-preview': 4.1.0 + '@vitest/browser-webdriverio': 4.1.0 + '@vitest/ui': 4.1.0 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue-component-type-helpers@2.2.12: + resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==} + + vue-eslint-parser@10.4.0: + resolution: {integrity: sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + + vue-router@4.6.4: + resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==} + peerDependencies: + vue: ^3.5.0 + + vue-tsc@2.2.0: + resolution: {integrity: sha512-gtmM1sUuJ8aSb0KoAFmK9yMxb8TxjewmxqTJ1aKphD5Cbu0rULFY6+UQT51zW7SpUcenfPUuflKyVwyx9Qdnxg==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.13: + resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + xmlbuilder@15.1.1: + resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} + engines: {node: '>=8.0'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yaml-eslint-parser@2.0.0: + resolution: {integrity: sha512-h0uDm97wvT2bokfwwTmY6kJ1hp6YDFL0nRHwNKz8s/VD1FH/vvZjAKoMUE+un0eaYBSG7/c6h+lJTP+31tjgTw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zip-stream@4.1.1: + resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} + engines: {node: '>= 10'} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + 7zip-bin@5.2.0: {} + + '@antfu/eslint-config@7.7.0(@typescript-eslint/rule-tester@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3))(@typescript-eslint/utils@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.29)(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.0(@types/node@22.19.15)(happy-dom@20.8.4)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2)))': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@clack/prompts': 1.1.0 + '@e18e/eslint-plugin': 0.2.0(eslint@9.39.4(jiti@2.6.1)) + '@eslint-community/eslint-plugin-eslint-comments': 4.7.1(eslint@9.39.4(jiti@2.6.1)) + '@eslint/markdown': 7.5.1 + '@stylistic/eslint-plugin': 5.10.0(eslint@9.39.4(jiti@2.6.1)) + '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@vitest/eslint-plugin': 1.6.9(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.0(@types/node@22.19.15)(happy-dom@20.8.4)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2))) + ansis: 4.2.0 + cac: 7.0.0 + eslint: 9.39.4(jiti@2.6.1) + eslint-config-flat-gitignore: 2.2.1(eslint@9.39.4(jiti@2.6.1)) + eslint-flat-config-utils: 3.0.2 + eslint-merge-processors: 2.0.0(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-antfu: 3.2.2(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-command: 3.5.2(@typescript-eslint/rule-tester@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3))(@typescript-eslint/utils@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-import-lite: 0.5.2(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-jsdoc: 62.7.1(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-jsonc: 3.1.1(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-n: 17.24.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-no-only-tests: 3.3.0 + eslint-plugin-perfectionist: 5.6.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-pnpm: 1.6.0(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-regexp: 3.0.0(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-toml: 1.3.1(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-unicorn: 63.0.0(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-vue: 10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@9.39.4(jiti@2.6.1)))(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@9.39.4(jiti@2.6.1))) + eslint-plugin-yml: 3.3.1(eslint@9.39.4(jiti@2.6.1)) + eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.29)(eslint@9.39.4(jiti@2.6.1)) + globals: 17.4.0 + local-pkg: 1.1.2 + parse-gitignore: 2.0.0 + toml-eslint-parser: 1.0.3 + vue-eslint-parser: 10.4.0(eslint@9.39.4(jiti@2.6.1)) + yaml-eslint-parser: 2.0.0 + transitivePeerDependencies: + - '@eslint/json' + - '@typescript-eslint/rule-tester' + - '@typescript-eslint/typescript-estree' + - '@typescript-eslint/utils' + - '@vue/compiler-sfc' + - oxlint + - supports-color + - typescript + - vitest + + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.6.0 + tinyexec: 1.0.2 + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@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.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.29.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.28.5': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bcoe/v8-coverage@1.0.2': {} + + '@clack/core@1.1.0': + dependencies: + sisteransi: 1.0.5 + + '@clack/prompts@1.1.0': + dependencies: + '@clack/core': 1.1.0 + sisteransi: 1.0.5 + + '@ctrl/tinycolor@4.2.0': {} + + '@develar/schema-utils@2.6.5': + dependencies: + ajv: 6.14.0 + ajv-keywords: 3.5.2(ajv@6.14.0) + + '@e18e/eslint-plugin@0.2.0(eslint@9.39.4(jiti@2.6.1))': + dependencies: + eslint-plugin-depend: 1.5.0(eslint@9.39.4(jiti@2.6.1)) + optionalDependencies: + eslint: 9.39.4(jiti@2.6.1) + + '@electron-toolkit/preload@2.0.0(electron@22.3.27)': + dependencies: + electron: 22.3.27 + + '@electron-toolkit/utils@2.0.0(electron@22.3.27)': + dependencies: + electron: 22.3.27 + + '@electron/asar@3.4.1': + dependencies: + commander: 5.1.0 + glob: 7.2.3 + minimatch: 3.1.5 + + '@electron/get@2.0.3': + dependencies: + debug: 4.4.3 + env-paths: 2.2.1 + fs-extra: 8.1.0 + got: 11.8.6 + progress: 2.0.3 + semver: 6.3.1 + sumchecker: 3.0.1 + optionalDependencies: + global-agent: 3.0.0 + transitivePeerDependencies: + - supports-color + + '@electron/notarize@2.5.0': + dependencies: + debug: 4.4.3 + fs-extra: 9.1.0 + promise-retry: 2.0.1 + transitivePeerDependencies: + - supports-color + + '@electron/osx-sign@1.3.1': + dependencies: + compare-version: 0.1.2 + debug: 4.4.3 + fs-extra: 10.1.0 + isbinaryfile: 4.0.10 + minimist: 1.2.8 + plist: 3.1.0 + transitivePeerDependencies: + - supports-color + + '@electron/rebuild@3.6.1': + dependencies: + '@malept/cross-spawn-promise': 2.0.0 + chalk: 4.1.2 + debug: 4.4.3 + detect-libc: 2.1.2 + fs-extra: 10.1.0 + got: 11.8.6 + node-abi: 3.87.0 + node-api-version: 0.2.1 + node-gyp: 9.4.1 + ora: 5.4.1 + read-binary-file-arch: 1.0.6 + semver: 7.7.4 + tar: 6.2.1 + yargs: 17.7.2 + transitivePeerDependencies: + - bluebird + - supports-color + + '@electron/universal@2.0.1': + dependencies: + '@electron/asar': 3.4.1 + '@malept/cross-spawn-promise': 2.0.0 + debug: 4.4.3 + dir-compare: 4.2.0 + fs-extra: 11.3.4 + minimatch: 9.0.9 + plist: 3.1.0 + transitivePeerDependencies: + - supports-color + + '@element-plus/icons-vue@2.3.2(vue@3.5.13(typescript@5.9.3))': + dependencies: + vue: 3.5.13(typescript@5.9.3) + + '@emnapi/core@1.8.1': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.8.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@es-joy/jsdoccomment@0.84.0': + dependencies: + '@types/estree': 1.0.8 + '@typescript-eslint/types': 8.56.1 + comment-parser: 1.4.5 + esquery: 1.7.0 + jsdoc-type-pratt-parser: 7.1.1 + + '@es-joy/resolve.exports@1.2.0': {} + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@eslint-community/eslint-plugin-eslint-comments@4.7.1(eslint@9.39.4(jiti@2.6.1))': + dependencies: + escape-string-regexp: 4.0.0 + eslint: 9.39.4(jiti@2.6.1) + ignore: 7.0.5 + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))': + dependencies: + eslint: 9.39.4(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/compat@2.0.3(eslint@9.39.4(jiti@2.6.1))': + dependencies: + '@eslint/core': 1.1.1 + optionalDependencies: + eslint: 9.39.4(jiti@2.6.1) + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/config-helpers@0.5.3': + dependencies: + '@eslint/core': 1.1.1 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/core@1.1.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.14.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.4': {} + + '@eslint/markdown@7.5.1': + dependencies: + '@eslint/core': 0.17.0 + '@eslint/plugin-kit': 0.4.1 + github-slugger: 2.0.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-frontmatter: 2.0.1 + mdast-util-gfm: 3.1.0 + micromark-extension-frontmatter: 2.0.0 + micromark-extension-gfm: 3.0.0 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@eslint/plugin-kit@0.6.1': + dependencies: + '@eslint/core': 1.1.1 + levn: 0.4.1 + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/utils@0.2.11': {} + + '@fortawesome/fontawesome-free@7.2.0': {} + + '@gar/promisify@1.1.3': {} + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@iconify/types@2.0.0': {} + + '@iconify/utils@3.1.0': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/types': 2.0.0 + mlly: 1.8.1 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@malept/cross-spawn-promise@2.0.0': + dependencies: + cross-spawn: 7.0.6 + + '@malept/flatpak-bundler@0.4.0': + dependencies: + debug: 4.4.3 + fs-extra: 9.1.0 + lodash: 4.17.23 + tmp-promise: 3.0.3 + transitivePeerDependencies: + - supports-color + + '@napi-rs/wasm-runtime@1.1.1': + dependencies: + '@emnapi/core': 1.8.1 + '@emnapi/runtime': 1.8.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@npmcli/fs@2.1.2': + dependencies: + '@gar/promisify': 1.1.3 + semver: 7.7.4 + + '@npmcli/move-file@2.0.1': + dependencies: + mkdirp: 1.0.4 + rimraf: 3.0.2 + + '@one-ini/wasm@0.1.1': {} + + '@ota-meshi/ast-token-store@0.3.0': {} + + '@oxc-parser/binding-android-arm-eabi@0.115.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.115.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.115.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.115.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.115.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.115.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.115.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.115.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.115.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.115.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.115.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.115.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.115.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.115.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.115.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.115.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.115.0': + dependencies: + '@napi-rs/wasm-runtime': 1.1.1 + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.115.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.115.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.115.0': + optional: true + + '@oxc-project/types@0.115.0': {} + + '@parcel/watcher-android-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-x64@2.5.6': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.6': + optional: true + + '@parcel/watcher-win32-arm64@2.5.6': + optional: true + + '@parcel/watcher-win32-ia32@2.5.6': + optional: true + + '@parcel/watcher-win32-x64@2.5.6': + optional: true + + '@parcel/watcher@2.5.6': + dependencies: + detect-libc: 2.1.2 + is-glob: 4.0.3 + node-addon-api: 7.1.1 + picomatch: 4.0.3 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.6 + '@parcel/watcher-darwin-arm64': 2.5.6 + '@parcel/watcher-darwin-x64': 2.5.6 + '@parcel/watcher-freebsd-x64': 2.5.6 + '@parcel/watcher-linux-arm-glibc': 2.5.6 + '@parcel/watcher-linux-arm-musl': 2.5.6 + '@parcel/watcher-linux-arm64-glibc': 2.5.6 + '@parcel/watcher-linux-arm64-musl': 2.5.6 + '@parcel/watcher-linux-x64-glibc': 2.5.6 + '@parcel/watcher-linux-x64-musl': 2.5.6 + '@parcel/watcher-win32-arm64': 2.5.6 + '@parcel/watcher-win32-ia32': 2.5.6 + '@parcel/watcher-win32-x64': 2.5.6 + optional: true + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@pkgr/core@0.2.9': {} + + '@playwright/test@1.58.2': + dependencies: + playwright: 1.58.2 + + '@polka/url@1.0.0-next.29': {} + + '@quansync/fs@1.0.0': + dependencies: + quansync: 1.0.0 + + '@rolldown/pluginutils@1.0.0-beta.29': {} + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@sindresorhus/base62@1.0.0': {} + + '@sindresorhus/is@4.6.0': {} + + '@standard-schema/spec@1.1.0': {} + + '@stylistic/eslint-plugin@5.10.0(eslint@9.39.4(jiti@2.6.1))': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + '@typescript-eslint/types': 8.56.1 + eslint: 9.39.4(jiti@2.6.1) + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + estraverse: 5.3.0 + picomatch: 4.0.3 + + '@sxzz/popperjs-es@2.11.8': {} + + '@szmarczak/http-timer@4.0.6': + dependencies: + defer-to-connect: 2.0.1 + + '@tauri-apps/api@2.10.1': {} + + '@tauri-apps/cli-darwin-arm64@2.10.0': + optional: true + + '@tauri-apps/cli-darwin-x64@2.10.0': + optional: true + + '@tauri-apps/cli-linux-arm-gnueabihf@2.10.0': + optional: true + + '@tauri-apps/cli-linux-arm64-gnu@2.10.0': + optional: true + + '@tauri-apps/cli-linux-arm64-musl@2.10.0': + optional: true + + '@tauri-apps/cli-linux-riscv64-gnu@2.10.0': + optional: true + + '@tauri-apps/cli-linux-x64-gnu@2.10.0': + optional: true + + '@tauri-apps/cli-linux-x64-musl@2.10.0': + optional: true + + '@tauri-apps/cli-win32-arm64-msvc@2.10.0': + optional: true + + '@tauri-apps/cli-win32-ia32-msvc@2.10.0': + optional: true + + '@tauri-apps/cli-win32-x64-msvc@2.10.0': + optional: true + + '@tauri-apps/cli@2.10.0': + optionalDependencies: + '@tauri-apps/cli-darwin-arm64': 2.10.0 + '@tauri-apps/cli-darwin-x64': 2.10.0 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.10.0 + '@tauri-apps/cli-linux-arm64-gnu': 2.10.0 + '@tauri-apps/cli-linux-arm64-musl': 2.10.0 + '@tauri-apps/cli-linux-riscv64-gnu': 2.10.0 + '@tauri-apps/cli-linux-x64-gnu': 2.10.0 + '@tauri-apps/cli-linux-x64-musl': 2.10.0 + '@tauri-apps/cli-win32-arm64-msvc': 2.10.0 + '@tauri-apps/cli-win32-ia32-msvc': 2.10.0 + '@tauri-apps/cli-win32-x64-msvc': 2.10.0 + + '@tauri-apps/plugin-dialog@2.6.0': + dependencies: + '@tauri-apps/api': 2.10.1 + + '@tauri-apps/plugin-fs@2.4.5': + dependencies: + '@tauri-apps/api': 2.10.1 + + '@tauri-apps/plugin-opener@2.5.3': + dependencies: + '@tauri-apps/api': 2.10.1 + + '@tauri-apps/plugin-os@2.3.2': + dependencies: + '@tauri-apps/api': 2.10.1 + + '@tootallnate/once@2.0.0': {} + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/cacheable-request@6.0.3': + dependencies: + '@types/http-cache-semantics': 4.2.0 + '@types/keyv': 3.1.4 + '@types/node': 22.19.15 + '@types/responselike': 1.0.3 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/deep-eql@4.0.2': {} + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.8': {} + + '@types/fs-extra@9.0.13': + dependencies: + '@types/node': 22.19.15 + + '@types/http-cache-semantics@4.2.0': {} + + '@types/json-schema@7.0.15': {} + + '@types/keyv@3.1.4': + dependencies: + '@types/node': 22.19.15 + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.24 + + '@types/lodash@4.17.24': {} + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + + '@types/node@16.18.126': {} + + '@types/node@20.19.37': + dependencies: + undici-types: 6.21.0 + + '@types/node@22.19.15': + dependencies: + undici-types: 6.21.0 + + '@types/plist@3.0.5': + dependencies: + '@types/node': 22.19.15 + xmlbuilder: 15.1.1 + optional: true + + '@types/responselike@1.0.3': + dependencies: + '@types/node': 22.19.15 + + '@types/unist@3.0.3': {} + + '@types/verror@1.10.11': + optional: true + + '@types/web-bluetooth@0.0.20': {} + + '@types/web-bluetooth@0.0.21': {} + + '@types/whatwg-mimetype@3.0.2': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 22.19.15 + + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 22.19.15 + optional: true + + '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/type-utils': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.56.1 + eslint: 9.39.4(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.56.1 + debug: 4.4.3 + eslint: 9.39.4(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) + '@typescript-eslint/types': 8.56.1 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/rule-tester@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/parser': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + ajv: 6.14.0 + eslint: 9.39.4(jiti@2.6.1) + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + semver: 7.7.4 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/scope-manager@8.56.1': + dependencies: + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/visitor-keys': 8.56.1 + + '@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.4(jiti@2.6.1) + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.56.1': {} + + '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.56.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/visitor-keys': 8.56.1 + debug: 4.4.3 + minimatch: 10.2.4 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + eslint: 9.39.4(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.56.1': + dependencies: + '@typescript-eslint/types': 8.56.1 + eslint-visitor-keys: 5.0.1 + + '@unocss/cli@66.6.6': + dependencies: + '@jridgewell/remapping': 2.3.5 + '@unocss/config': 66.6.6 + '@unocss/core': 66.6.6 + '@unocss/preset-wind3': 66.6.6 + '@unocss/preset-wind4': 66.6.6 + '@unocss/transformer-directives': 66.6.6 + cac: 6.7.14 + chokidar: 5.0.0 + colorette: 2.0.20 + consola: 3.4.2 + magic-string: 0.30.21 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + tinyglobby: 0.2.15 + unplugin-utils: 0.3.1 + + '@unocss/config@66.6.6': + dependencies: + '@unocss/core': 66.6.6 + colorette: 2.0.20 + consola: 3.4.2 + unconfig: 7.5.0 + + '@unocss/core@0.62.4': {} + + '@unocss/core@66.6.6': {} + + '@unocss/extractor-arbitrary-variants@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + + '@unocss/extractor-arbitrary-variants@66.6.6': + dependencies: + '@unocss/core': 66.6.6 + + '@unocss/inspector@66.6.6': + dependencies: + '@unocss/core': 66.6.6 + '@unocss/rule-utils': 66.6.6 + colorette: 2.0.20 + gzip-size: 6.0.0 + sirv: 3.0.2 + + '@unocss/preset-attributify@66.6.6': + dependencies: + '@unocss/core': 66.6.6 + + '@unocss/preset-icons@66.6.6': + dependencies: + '@iconify/utils': 3.1.0 + '@unocss/core': 66.6.6 + ofetch: 1.5.1 + + '@unocss/preset-mini@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + '@unocss/extractor-arbitrary-variants': 0.62.4 + '@unocss/rule-utils': 0.62.4 + + '@unocss/preset-mini@66.6.6': + dependencies: + '@unocss/core': 66.6.6 + '@unocss/extractor-arbitrary-variants': 66.6.6 + '@unocss/rule-utils': 66.6.6 + + '@unocss/preset-tagify@66.6.6': + dependencies: + '@unocss/core': 66.6.6 + + '@unocss/preset-typography@66.6.6': + dependencies: + '@unocss/core': 66.6.6 + '@unocss/rule-utils': 66.6.6 + + '@unocss/preset-uno@66.6.6': + dependencies: + '@unocss/core': 66.6.6 + '@unocss/preset-wind3': 66.6.6 + + '@unocss/preset-web-fonts@66.6.6': + dependencies: + '@unocss/core': 66.6.6 + ofetch: 1.5.1 + + '@unocss/preset-wind3@66.6.6': + dependencies: + '@unocss/core': 66.6.6 + '@unocss/preset-mini': 66.6.6 + '@unocss/rule-utils': 66.6.6 + + '@unocss/preset-wind4@66.6.6': + dependencies: + '@unocss/core': 66.6.6 + '@unocss/extractor-arbitrary-variants': 66.6.6 + '@unocss/rule-utils': 66.6.6 + + '@unocss/preset-wind@66.6.6': + dependencies: + '@unocss/core': 66.6.6 + '@unocss/preset-wind3': 66.6.6 + + '@unocss/rule-utils@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + magic-string: 0.30.21 + + '@unocss/rule-utils@66.6.6': + dependencies: + '@unocss/core': 66.6.6 + magic-string: 0.30.21 + + '@unocss/transformer-attributify-jsx@66.6.6': + dependencies: + '@unocss/core': 66.6.6 + oxc-parser: 0.115.0 + oxc-walker: 0.7.0(oxc-parser@0.115.0) + + '@unocss/transformer-compile-class@66.6.6': + dependencies: + '@unocss/core': 66.6.6 + + '@unocss/transformer-directives@66.6.6': + dependencies: + '@unocss/core': 66.6.6 + '@unocss/rule-utils': 66.6.6 + css-tree: 3.2.1 + + '@unocss/transformer-variant-group@66.6.6': + dependencies: + '@unocss/core': 66.6.6 + + '@unocss/vite@66.6.6(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2))': + dependencies: + '@jridgewell/remapping': 2.3.5 + '@unocss/config': 66.6.6 + '@unocss/core': 66.6.6 + '@unocss/inspector': 66.6.6 + chokidar: 5.0.0 + magic-string: 0.30.21 + pathe: 2.0.3 + tinyglobby: 0.2.15 + unplugin-utils: 0.3.1 + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2) + + '@vitejs/plugin-vue-jsx@4.1.1(vite@5.4.11(@types/node@22.19.15)(sass@1.97.3))(vue@3.5.13(typescript@5.9.3))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.29.0) + vite: 5.4.11(@types/node@22.19.15)(sass@1.97.3) + vue: 3.5.13(typescript@5.9.3) + transitivePeerDependencies: + - supports-color + + '@vitejs/plugin-vue-jsx@4.1.1(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.29.0) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2) + vue: 3.5.13(typescript@5.9.3) + transitivePeerDependencies: + - supports-color + + '@vitejs/plugin-vue@6.0.1(vite@5.4.11(@types/node@22.19.15)(sass@1.97.3))(vue@3.5.13(typescript@5.9.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.29 + vite: 5.4.11(@types/node@22.19.15)(sass@1.97.3) + vue: 3.5.13(typescript@5.9.3) + + '@vitejs/plugin-vue@6.0.1(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.29 + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2) + vue: 3.5.13(typescript@5.9.3) + + '@vitest/coverage-v8@4.1.0(vitest@4.1.0(@types/node@22.19.15)(happy-dom@20.8.4)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2)))': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.1.0 + ast-v8-to-istanbul: 1.0.0 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.2 + obug: 2.1.1 + std-env: 4.0.0 + tinyrainbow: 3.1.0 + vitest: 4.1.0(@types/node@22.19.15)(happy-dom@20.8.4)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2)) + + '@vitest/eslint-plugin@1.6.9(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.0(@types/node@22.19.15)(happy-dom@20.8.4)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2)))': + dependencies: + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/utils': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.4(jiti@2.6.1) + optionalDependencies: + typescript: 5.9.3 + vitest: 4.1.0(@types/node@22.19.15)(happy-dom@20.8.4)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2)) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@4.1.0': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.0 + '@vitest/utils': 4.1.0 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2))': + dependencies: + '@vitest/spy': 4.1.0 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2) + + '@vitest/pretty-format@4.1.0': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.0': + dependencies: + '@vitest/utils': 4.1.0 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.0': + dependencies: + '@vitest/pretty-format': 4.1.0 + '@vitest/utils': 4.1.0 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.0': {} + + '@vitest/utils@4.1.0': + dependencies: + '@vitest/pretty-format': 4.1.0 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + '@volar/language-core@2.4.28': + dependencies: + '@volar/source-map': 2.4.28 + + '@volar/source-map@2.4.28': {} + + '@volar/typescript@2.4.28': + dependencies: + '@volar/language-core': 2.4.28 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/babel-helper-vue-transform-on@1.5.0': {} + + '@vue/babel-plugin-jsx@1.5.0(@babel/core@7.29.0)': + dependencies: + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@vue/babel-helper-vue-transform-on': 1.5.0 + '@vue/babel-plugin-resolve-type': 1.5.0(@babel/core@7.29.0) + '@vue/shared': 3.5.29 + optionalDependencies: + '@babel/core': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@vue/babel-plugin-resolve-type@1.5.0(@babel/core@7.29.0)': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/parser': 7.29.0 + '@vue/compiler-sfc': 3.5.29 + transitivePeerDependencies: + - supports-color + + '@vue/compiler-core@3.5.13': + dependencies: + '@babel/parser': 7.29.0 + '@vue/shared': 3.5.13 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-core@3.5.29': + dependencies: + '@babel/parser': 7.29.0 + '@vue/shared': 3.5.29 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.13': + dependencies: + '@vue/compiler-core': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/compiler-dom@3.5.29': + dependencies: + '@vue/compiler-core': 3.5.29 + '@vue/shared': 3.5.29 + + '@vue/compiler-sfc@3.5.13': + dependencies: + '@babel/parser': 7.29.0 + '@vue/compiler-core': 3.5.13 + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-ssr': 3.5.13 + '@vue/shared': 3.5.13 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.8 + source-map-js: 1.2.1 + + '@vue/compiler-sfc@3.5.29': + dependencies: + '@babel/parser': 7.29.0 + '@vue/compiler-core': 3.5.29 + '@vue/compiler-dom': 3.5.29 + '@vue/compiler-ssr': 3.5.29 + '@vue/shared': 3.5.29 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.8 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.13': + dependencies: + '@vue/compiler-dom': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/compiler-ssr@3.5.29': + dependencies: + '@vue/compiler-dom': 3.5.29 + '@vue/shared': 3.5.29 + + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + '@vue/devtools-api@6.6.4': {} + + '@vue/devtools-api@7.7.9': + dependencies: + '@vue/devtools-kit': 7.7.9 + + '@vue/devtools-kit@7.7.9': + dependencies: + '@vue/devtools-shared': 7.7.9 + birpc: 2.9.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.6 + + '@vue/devtools-shared@7.7.9': + dependencies: + rfdc: 1.4.1 + + '@vue/language-core@2.2.0(typescript@5.9.3)': + dependencies: + '@volar/language-core': 2.4.28 + '@vue/compiler-dom': 3.5.29 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.29 + alien-signals: 0.4.14 + minimatch: 9.0.9 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.9.3 + + '@vue/reactivity@3.5.13': + dependencies: + '@vue/shared': 3.5.13 + + '@vue/runtime-core@3.5.13': + dependencies: + '@vue/reactivity': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/runtime-dom@3.5.13': + dependencies: + '@vue/reactivity': 3.5.13 + '@vue/runtime-core': 3.5.13 + '@vue/shared': 3.5.13 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.9.3))': + dependencies: + '@vue/compiler-ssr': 3.5.13 + '@vue/shared': 3.5.13 + vue: 3.5.13(typescript@5.9.3) + + '@vue/shared@3.5.13': {} + + '@vue/shared@3.5.29': {} + + '@vue/test-utils@2.4.6': + dependencies: + js-beautify: 1.15.4 + vue-component-type-helpers: 2.2.12 + + '@vueuse/core@12.0.0(typescript@5.9.3)': + dependencies: + '@types/web-bluetooth': 0.0.20 + '@vueuse/metadata': 12.0.0 + '@vueuse/shared': 12.0.0(typescript@5.9.3) + vue: 3.5.13(typescript@5.9.3) + transitivePeerDependencies: + - typescript + + '@vueuse/core@12.8.2(typescript@5.9.3)': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 12.8.2 + '@vueuse/shared': 12.8.2(typescript@5.9.3) + vue: 3.5.13(typescript@5.9.3) + transitivePeerDependencies: + - typescript + + '@vueuse/metadata@12.0.0': {} + + '@vueuse/metadata@12.8.2': {} + + '@vueuse/shared@12.0.0(typescript@5.9.3)': + dependencies: + vue: 3.5.13(typescript@5.9.3) + transitivePeerDependencies: + - typescript + + '@vueuse/shared@12.8.2(typescript@5.9.3)': + dependencies: + vue: 3.5.13(typescript@5.9.3) + transitivePeerDependencies: + - typescript + + '@xmldom/xmldom@0.8.11': {} + + abbrev@1.1.1: {} + + abbrev@2.0.0: {} + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + agent-base@7.1.4: {} + + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + + ajv-keywords@3.5.2(ajv@6.14.0): + dependencies: + ajv: 6.14.0 + + ajv@6.14.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + alien-signals@0.4.14: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + ansis@4.2.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + app-builder-bin@5.0.0-alpha.10: {} + + app-builder-lib@25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8): + dependencies: + '@develar/schema-utils': 2.6.5 + '@electron/notarize': 2.5.0 + '@electron/osx-sign': 1.3.1 + '@electron/rebuild': 3.6.1 + '@electron/universal': 2.0.1 + '@malept/flatpak-bundler': 0.4.0 + '@types/fs-extra': 9.0.13 + async-exit-hook: 2.0.1 + bluebird-lst: 1.0.9 + builder-util: 25.1.7 + builder-util-runtime: 9.2.10 + chromium-pickle-js: 0.2.0 + config-file-ts: 0.2.8-rc1 + debug: 4.4.3 + dmg-builder: 25.1.8(electron-builder-squirrel-windows@25.1.8) + dotenv: 16.6.1 + dotenv-expand: 11.0.7 + ejs: 3.1.10 + electron-builder-squirrel-windows: 25.1.8(dmg-builder@25.1.8) + electron-publish: 25.1.7 + form-data: 4.0.5 + fs-extra: 10.1.0 + hosted-git-info: 4.1.0 + is-ci: 3.0.1 + isbinaryfile: 5.0.7 + js-yaml: 4.1.1 + json5: 2.2.3 + lazy-val: 1.0.5 + minimatch: 10.2.4 + resedit: 1.7.2 + sanitize-filename: 1.6.3 + semver: 7.7.4 + tar: 6.2.1 + temp-file: 3.4.0 + transitivePeerDependencies: + - bluebird + - supports-color + + aproba@2.1.0: {} + + archiver-utils@2.1.0: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 2.3.8 + + archiver-utils@3.0.4: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + + archiver@5.3.2: + dependencies: + archiver-utils: 2.1.0 + async: 3.2.6 + buffer-crc32: 0.2.13 + readable-stream: 3.6.2 + readdir-glob: 1.1.3 + tar-stream: 2.2.0 + zip-stream: 4.1.1 + + are-docs-informative@0.0.2: {} + + are-we-there-yet@3.0.1: + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + + argparse@2.0.1: {} + + assert-plus@1.0.0: + optional: true + + assertion-error@2.0.1: {} + + ast-v8-to-istanbul@1.0.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + + astral-regex@2.0.0: + optional: true + + async-exit-hook@2.0.1: {} + + async-validator@4.2.5: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + at-least-node@1.0.0: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.10.0: {} + + binary-extensions@2.3.0: {} + + birpc@2.9.0: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + bluebird-lst@1.0.9: + dependencies: + bluebird: 3.7.2 + + bluebird@3.7.2: {} + + boolbase@1.0.0: {} + + boolean@3.2.0: + optional: true + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.4: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.10.0 + caniuse-lite: 1.0.30001777 + electron-to-chromium: 1.5.307 + node-releases: 2.0.36 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + buffer-crc32@0.2.13: {} + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + builder-util-runtime@9.2.10: + dependencies: + debug: 4.4.3 + sax: 1.5.0 + transitivePeerDependencies: + - supports-color + + builder-util@25.1.7: + dependencies: + 7zip-bin: 5.2.0 + '@types/debug': 4.1.12 + app-builder-bin: 5.0.0-alpha.10 + bluebird-lst: 1.0.9 + builder-util-runtime: 9.2.10 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + fs-extra: 10.1.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-ci: 3.0.1 + js-yaml: 4.1.1 + source-map-support: 0.5.21 + stat-mode: 1.0.0 + temp-file: 3.4.0 + transitivePeerDependencies: + - supports-color + + builtin-modules@5.0.0: {} + + cac@6.7.14: {} + + cac@7.0.0: {} + + cacache@16.1.3: + dependencies: + '@npmcli/fs': 2.1.2 + '@npmcli/move-file': 2.0.1 + chownr: 2.0.0 + fs-minipass: 2.1.0 + glob: 8.1.0 + infer-owner: 1.0.4 + lru-cache: 7.18.3 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + mkdirp: 1.0.4 + p-map: 4.0.0 + promise-inflight: 1.0.1 + rimraf: 3.0.2 + ssri: 9.0.1 + tar: 6.2.1 + unique-filename: 2.0.1 + transitivePeerDependencies: + - bluebird + + cacheable-lookup@5.0.4: {} + + cacheable-request@7.0.4: + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.2.0 + keyv: 4.5.4 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.1 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001777: {} + + ccount@2.0.1: {} + + chai@6.2.2: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + change-case@5.4.4: {} + + character-entities@2.0.2: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + chownr@2.0.0: {} + + chromium-pickle-js@0.2.0: {} + + ci-info@3.9.0: {} + + ci-info@4.4.0: {} + + clean-regexp@1.0.0: + dependencies: + escape-string-regexp: 1.0.5 + + clean-stack@2.2.0: {} + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-spinners@2.9.2: {} + + cli-truncate@2.1.0: + dependencies: + slice-ansi: 3.0.0 + string-width: 4.2.3 + optional: true + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone-response@1.0.3: + dependencies: + mimic-response: 1.0.1 + + clone@1.0.4: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-support@1.1.3: {} + + colorette@2.0.20: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@10.0.1: {} + + commander@5.1.0: {} + + comment-parser@1.4.5: {} + + compare-version@0.1.2: {} + + compress-commons@4.1.2: + dependencies: + buffer-crc32: 0.2.13 + crc32-stream: 4.0.3 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + + concat-map@0.0.1: {} + + confbox@0.1.8: {} + + confbox@0.2.4: {} + + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + + config-file-ts@0.2.8-rc1: + dependencies: + glob: 10.4.5 + typescript: 5.9.3 + + consola@3.4.2: {} + + console-control-strings@1.1.0: {} + + convert-source-map@2.0.0: {} + + copy-anything@4.0.5: + dependencies: + is-what: 5.5.0 + + core-js-compat@3.48.0: + dependencies: + browserslist: 4.28.1 + + core-util-is@1.0.2: + optional: true + + core-util-is@1.0.3: {} + + crc-32@1.2.2: {} + + crc32-stream@4.0.3: + dependencies: + crc-32: 1.2.2 + readable-stream: 3.6.2 + + crc@3.8.0: + dependencies: + buffer: 5.7.1 + optional: true + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + dayjs@1.11.19: {} + + de-indent@1.0.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-is@0.1.4: {} + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + defer-to-connect@2.0.1: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + optional: true + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + optional: true + + defu@6.1.4: {} + + delayed-stream@1.0.0: {} + + delegates@1.0.0: {} + + dequal@2.0.3: {} + + destr@2.0.5: {} + + detect-libc@2.1.2: {} + + detect-node@2.1.0: + optional: true + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + diff-sequences@29.6.3: {} + + dir-compare@4.2.0: + dependencies: + minimatch: 3.1.5 + p-limit: 3.1.0 + + dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8): + dependencies: + app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8) + builder-util: 25.1.7 + builder-util-runtime: 9.2.10 + fs-extra: 10.1.0 + iconv-lite: 0.6.3 + js-yaml: 4.1.1 + optionalDependencies: + dmg-license: 1.0.11 + transitivePeerDependencies: + - bluebird + - electron-builder-squirrel-windows + - supports-color + + dmg-license@1.0.11: + dependencies: + '@types/plist': 3.0.5 + '@types/verror': 1.10.11 + ajv: 6.14.0 + crc: 3.8.0 + iconv-corefoundation: 1.1.7 + plist: 3.1.0 + smart-buffer: 4.2.0 + verror: 1.10.1 + optional: true + + dotenv-expand@11.0.7: + dependencies: + dotenv: 16.6.1 + + dotenv@16.6.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + duplexer@0.1.2: {} + + eastasianwidth@0.2.0: {} + + editorconfig@1.0.7: + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.9 + semver: 7.7.4 + + ejs@3.1.10: + dependencies: + jake: 10.9.4 + + electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8): + dependencies: + app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8) + archiver: 5.3.2 + builder-util: 25.1.7 + fs-extra: 10.1.0 + transitivePeerDependencies: + - bluebird + - dmg-builder + - supports-color + + electron-builder@25.1.8(electron-builder-squirrel-windows@25.1.8): + dependencies: + app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8) + builder-util: 25.1.7 + builder-util-runtime: 9.2.10 + chalk: 4.1.2 + dmg-builder: 25.1.8(electron-builder-squirrel-windows@25.1.8) + fs-extra: 10.1.0 + is-ci: 3.0.1 + lazy-val: 1.0.5 + simple-update-notifier: 2.0.0 + yargs: 17.7.2 + transitivePeerDependencies: + - bluebird + - electron-builder-squirrel-windows + - supports-color + + electron-publish@25.1.7: + dependencies: + '@types/fs-extra': 9.0.13 + builder-util: 25.1.7 + builder-util-runtime: 9.2.10 + chalk: 4.1.2 + fs-extra: 10.1.0 + lazy-val: 1.0.5 + mime: 2.6.0 + transitivePeerDependencies: + - supports-color + + electron-to-chromium@1.5.307: {} + + electron-updater@6.3.9: + dependencies: + builder-util-runtime: 9.2.10 + fs-extra: 10.1.0 + js-yaml: 4.1.1 + lazy-val: 1.0.5 + lodash.escaperegexp: 4.1.2 + lodash.isequal: 4.5.0 + semver: 7.7.4 + tiny-typed-emitter: 2.1.0 + transitivePeerDependencies: + - supports-color + + electron-vite@2.3.0(vite@5.4.11(@types/node@22.19.15)(sass@1.97.3)): + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) + cac: 6.7.14 + esbuild: 0.21.5 + magic-string: 0.30.21 + picocolors: 1.1.1 + vite: 5.4.11(@types/node@22.19.15)(sass@1.97.3) + transitivePeerDependencies: + - supports-color + + electron@22.3.27: + dependencies: + '@electron/get': 2.0.3 + '@types/node': 16.18.126 + extract-zip: 2.0.1 + transitivePeerDependencies: + - supports-color + + electron@33.4.11: + dependencies: + '@electron/get': 2.0.3 + '@types/node': 20.19.37 + extract-zip: 2.0.1 + transitivePeerDependencies: + - supports-color + + element-plus@2.13.5(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)): + dependencies: + '@ctrl/tinycolor': 4.2.0 + '@element-plus/icons-vue': 2.3.2(vue@3.5.13(typescript@5.9.3)) + '@floating-ui/dom': 1.7.6 + '@popperjs/core': '@sxzz/popperjs-es@2.11.8' + '@types/lodash': 4.17.24 + '@types/lodash-es': 4.17.12 + '@vueuse/core': 12.0.0(typescript@5.9.3) + async-validator: 4.2.5 + dayjs: 1.11.19 + lodash: 4.17.23 + lodash-es: 4.17.23 + lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.23)(lodash@4.17.23) + memoize-one: 6.0.0 + normalize-wheel-es: 1.2.0 + vue: 3.5.13(typescript@5.9.3) + transitivePeerDependencies: + - typescript + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + empathic@2.0.0: {} + + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + optional: true + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + enhanced-resolve@5.20.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + entities@4.5.0: {} + + entities@7.0.1: {} + + env-paths@2.2.1: {} + + err-code@2.0.3: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@2.0.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-toolkit@1.45.1: {} + + es6-error@4.1.1: + optional: true + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + escalade@3.2.0: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + eslint-compat-utils@0.5.1(eslint@9.39.4(jiti@2.6.1)): + dependencies: + eslint: 9.39.4(jiti@2.6.1) + semver: 7.7.4 + + eslint-config-flat-gitignore@2.2.1(eslint@9.39.4(jiti@2.6.1)): + dependencies: + '@eslint/compat': 2.0.3(eslint@9.39.4(jiti@2.6.1)) + eslint: 9.39.4(jiti@2.6.1) + + eslint-flat-config-utils@3.0.2: + dependencies: + '@eslint/config-helpers': 0.5.3 + pathe: 2.0.3 + + eslint-json-compat-utils@0.2.2(eslint@9.39.4(jiti@2.6.1))(jsonc-eslint-parser@3.1.0): + dependencies: + eslint: 9.39.4(jiti@2.6.1) + esquery: 1.7.0 + jsonc-eslint-parser: 3.1.0 + + eslint-merge-processors@2.0.0(eslint@9.39.4(jiti@2.6.1)): + dependencies: + eslint: 9.39.4(jiti@2.6.1) + + eslint-plugin-antfu@3.2.2(eslint@9.39.4(jiti@2.6.1)): + dependencies: + eslint: 9.39.4(jiti@2.6.1) + + eslint-plugin-command@3.5.2(@typescript-eslint/rule-tester@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3))(@typescript-eslint/utils@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)): + dependencies: + '@es-joy/jsdoccomment': 0.84.0 + '@typescript-eslint/rule-tester': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.4(jiti@2.6.1) + + eslint-plugin-depend@1.5.0(eslint@9.39.4(jiti@2.6.1)): + dependencies: + empathic: 2.0.0 + eslint: 9.39.4(jiti@2.6.1) + module-replacements: 2.11.0 + semver: 7.7.4 + + eslint-plugin-es-x@7.8.0(eslint@9.39.4(jiti@2.6.1)): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + eslint: 9.39.4(jiti@2.6.1) + eslint-compat-utils: 0.5.1(eslint@9.39.4(jiti@2.6.1)) + + eslint-plugin-import-lite@0.5.2(eslint@9.39.4(jiti@2.6.1)): + dependencies: + eslint: 9.39.4(jiti@2.6.1) + + eslint-plugin-jsdoc@62.7.1(eslint@9.39.4(jiti@2.6.1)): + dependencies: + '@es-joy/jsdoccomment': 0.84.0 + '@es-joy/resolve.exports': 1.2.0 + are-docs-informative: 0.0.2 + comment-parser: 1.4.5 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint: 9.39.4(jiti@2.6.1) + espree: 11.2.0 + esquery: 1.7.0 + html-entities: 2.6.0 + object-deep-merge: 2.0.0 + parse-imports-exports: 0.2.4 + semver: 7.7.4 + spdx-expression-parse: 4.0.0 + to-valid-identifier: 1.0.0 + transitivePeerDependencies: + - supports-color + + eslint-plugin-jsonc@3.1.1(eslint@9.39.4(jiti@2.6.1)): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + '@eslint/core': 1.1.1 + '@eslint/plugin-kit': 0.6.1 + '@ota-meshi/ast-token-store': 0.3.0 + diff-sequences: 29.6.3 + eslint: 9.39.4(jiti@2.6.1) + eslint-json-compat-utils: 0.2.2(eslint@9.39.4(jiti@2.6.1))(jsonc-eslint-parser@3.1.0) + jsonc-eslint-parser: 3.1.0 + natural-compare: 1.4.0 + synckit: 0.11.12 + transitivePeerDependencies: + - '@eslint/json' + + eslint-plugin-n@17.24.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + enhanced-resolve: 5.20.0 + eslint: 9.39.4(jiti@2.6.1) + eslint-plugin-es-x: 7.8.0(eslint@9.39.4(jiti@2.6.1)) + get-tsconfig: 4.13.6 + globals: 15.15.0 + globrex: 0.1.2 + ignore: 5.3.2 + semver: 7.7.4 + ts-declaration-location: 1.0.7(typescript@5.9.3) + transitivePeerDependencies: + - typescript + + eslint-plugin-no-only-tests@3.3.0: {} + + eslint-plugin-perfectionist@5.6.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@typescript-eslint/utils': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.4(jiti@2.6.1) + natural-orderby: 5.0.0 + transitivePeerDependencies: + - supports-color + - typescript + + eslint-plugin-pnpm@1.6.0(eslint@9.39.4(jiti@2.6.1)): + dependencies: + empathic: 2.0.0 + eslint: 9.39.4(jiti@2.6.1) + jsonc-eslint-parser: 3.1.0 + pathe: 2.0.3 + pnpm-workspace-yaml: 1.6.0 + tinyglobby: 0.2.15 + yaml: 2.8.2 + yaml-eslint-parser: 2.0.0 + + eslint-plugin-regexp@3.0.0(eslint@9.39.4(jiti@2.6.1)): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + comment-parser: 1.4.5 + eslint: 9.39.4(jiti@2.6.1) + jsdoc-type-pratt-parser: 7.1.1 + refa: 0.12.1 + regexp-ast-analysis: 0.7.1 + scslre: 0.3.0 + + eslint-plugin-toml@1.3.1(eslint@9.39.4(jiti@2.6.1)): + dependencies: + '@eslint/core': 1.1.1 + '@eslint/plugin-kit': 0.6.1 + '@ota-meshi/ast-token-store': 0.3.0 + debug: 4.4.3 + eslint: 9.39.4(jiti@2.6.1) + toml-eslint-parser: 1.0.3 + transitivePeerDependencies: + - supports-color + + eslint-plugin-unicorn@63.0.0(eslint@9.39.4(jiti@2.6.1)): + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + change-case: 5.4.4 + ci-info: 4.4.0 + clean-regexp: 1.0.0 + core-js-compat: 3.48.0 + eslint: 9.39.4(jiti@2.6.1) + find-up-simple: 1.0.1 + globals: 16.5.0 + indent-string: 5.0.0 + is-builtin-module: 5.0.0 + jsesc: 3.1.0 + pluralize: 8.0.0 + regexp-tree: 0.1.27 + regjsparser: 0.13.0 + semver: 7.7.4 + strip-indent: 4.1.1 + + eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)): + dependencies: + eslint: 9.39.4(jiti@2.6.1) + optionalDependencies: + '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + + eslint-plugin-vue@10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@9.39.4(jiti@2.6.1)))(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@9.39.4(jiti@2.6.1))): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + eslint: 9.39.4(jiti@2.6.1) + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 7.1.1 + semver: 7.7.4 + vue-eslint-parser: 10.4.0(eslint@9.39.4(jiti@2.6.1)) + xml-name-validator: 4.0.0 + optionalDependencies: + '@stylistic/eslint-plugin': 5.10.0(eslint@9.39.4(jiti@2.6.1)) + '@typescript-eslint/parser': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + + eslint-plugin-yml@3.3.1(eslint@9.39.4(jiti@2.6.1)): + dependencies: + '@eslint/core': 1.1.1 + '@eslint/plugin-kit': 0.6.1 + '@ota-meshi/ast-token-store': 0.3.0 + debug: 4.4.3 + diff-sequences: 29.6.3 + escape-string-regexp: 5.0.0 + eslint: 9.39.4(jiti@2.6.1) + natural-compare: 1.4.0 + yaml-eslint-parser: 2.0.0 + transitivePeerDependencies: + - supports-color + + eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.29)(eslint@9.39.4(jiti@2.6.1)): + dependencies: + '@vue/compiler-sfc': 3.5.29 + eslint: 9.39.4(jiti@2.6.1) + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.8 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.4(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.14.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + espree@11.2.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + expect-type@1.3.0: {} + + exponential-backoff@3.1.3: {} + + exsolve@1.0.8: {} + + extract-zip@2.0.1: + dependencies: + debug: 4.4.3 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + + extsprintf@1.4.1: + optional: true + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fault@2.0.1: + dependencies: + format: 0.2.2 + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + filelist@1.0.6: + dependencies: + minimatch: 5.1.9 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up-simple@1.0.1: {} + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.0 + keyv: 4.5.4 + + flatted@3.4.0: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + format@0.2.2: {} + + fs-constants@1.0.0: {} + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-extra@11.3.4: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs.realpath@1.0.0: {} + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gauge@4.0.4: + dependencies: + aproba: 2.1.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@5.2.0: + dependencies: + pump: 3.0.4 + + get-tsconfig@4.13.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + github-slugger@2.0.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + glob@8.1.0: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.9 + once: 1.4.0 + + global-agent@3.0.0: + dependencies: + boolean: 3.2.0 + es6-error: 4.1.1 + matcher: 3.0.0 + roarr: 2.15.4 + semver: 7.7.4 + serialize-error: 7.0.1 + optional: true + + globals@14.0.0: {} + + globals@15.15.0: {} + + globals@16.5.0: {} + + globals@17.4.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + optional: true + + globrex@0.1.2: {} + + gopd@1.2.0: {} + + got@11.8.6: + dependencies: + '@sindresorhus/is': 4.6.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.3 + '@types/responselike': 1.0.3 + cacheable-lookup: 5.0.4 + cacheable-request: 7.0.4 + decompress-response: 6.0.0 + http2-wrapper: 1.0.3 + lowercase-keys: 2.0.0 + p-cancelable: 2.1.1 + responselike: 2.0.1 + + graceful-fs@4.2.11: {} + + gzip-size@6.0.0: + dependencies: + duplexer: 0.1.2 + + happy-dom@20.8.4: + dependencies: + '@types/node': 22.19.15 + '@types/whatwg-mimetype': 3.0.2 + '@types/ws': 8.18.1 + entities: 7.0.1 + whatwg-mimetype: 3.0.0 + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + optional: true + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + has-unicode@2.0.1: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + he@1.2.0: {} + + hookable@5.5.3: {} + + hosted-git-info@4.1.0: + dependencies: + lru-cache: 6.0.0 + + html-entities@2.6.0: {} + + html-escaper@2.0.2: {} + + http-cache-semantics@4.2.0: {} + + http-proxy-agent@5.0.0: + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + http2-wrapper@1.0.3: + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + + iconv-corefoundation@1.1.7: + dependencies: + cli-truncate: 2.1.0 + node-addon-api: 1.7.2 + optional: true + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + immutable@5.1.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + indent-string@5.0.0: {} + + infer-owner@1.0.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ip-address@10.1.0: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-builtin-module@5.0.0: + dependencies: + builtin-modules: 5.0.0 + + is-ci@3.0.1: + dependencies: + ci-info: 3.9.0 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-interactive@1.0.0: {} + + is-lambda@1.0.1: {} + + is-number@7.0.0: {} + + is-unicode-supported@0.1.0: {} + + is-what@5.5.0: {} + + isarray@1.0.0: {} + + isbinaryfile@4.0.10: {} + + isbinaryfile@5.0.7: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jake@10.9.4: + dependencies: + async: 3.2.6 + filelist: 1.0.6 + picocolors: 1.1.1 + + jiti@2.6.1: {} + + js-beautify@1.15.4: + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.7 + glob: 10.4.5 + js-cookie: 3.0.5 + nopt: 7.2.1 + + js-cookie@3.0.5: {} + + js-tokens@10.0.0: {} + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsdoc-type-pratt-parser@7.1.1: {} + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json-stringify-safe@5.0.1: + optional: true + + json5@2.2.3: {} + + jsonc-eslint-parser@3.1.0: + dependencies: + acorn: 8.16.0 + eslint-visitor-keys: 5.0.1 + semver: 7.7.4 + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + lazy-val@1.0.5: {} + + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + local-pkg@1.1.2: + dependencies: + mlly: 1.8.1 + pkg-types: 2.3.0 + quansync: 0.2.11 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash-es@4.17.23: {} + + lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.23)(lodash@4.17.23): + dependencies: + '@types/lodash-es': 4.17.12 + lodash: 4.17.23 + lodash-es: 4.17.23 + + lodash.defaults@4.2.0: {} + + lodash.difference@4.5.0: {} + + lodash.escaperegexp@4.1.2: {} + + lodash.flatten@4.4.0: {} + + lodash.isequal@4.5.0: {} + + lodash.isplainobject@4.0.6: {} + + lodash.merge@4.6.2: {} + + lodash.union@4.6.0: {} + + lodash@4.17.23: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + longest-streak@3.1.0: {} + + lowercase-keys@2.0.0: {} + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + lru-cache@7.18.3: {} + + magic-regexp@0.10.0: + dependencies: + estree-walker: 3.0.3 + magic-string: 0.30.21 + mlly: 1.8.1 + regexp-tree: 0.1.27 + type-level-regexp: 0.1.17 + ufo: 1.6.3 + unplugin: 2.3.11 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.5.2: + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.4 + + make-fetch-happen@10.2.1: + dependencies: + agentkeepalive: 4.6.0 + cacache: 16.1.3 + http-cache-semantics: 4.2.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-lambda: 1.0.1 + lru-cache: 7.18.3 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-fetch: 2.1.2 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + negotiator: 0.6.4 + promise-retry: 2.0.1 + socks-proxy-agent: 7.0.0 + ssri: 9.0.1 + transitivePeerDependencies: + - bluebird + - supports-color + + markdown-table@3.0.4: {} + + matcher@3.0.0: + dependencies: + escape-string-regexp: 4.0.0 + optional: true + + math-intrinsics@1.1.0: {} + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-frontmatter@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + escape-string-regexp: 5.0.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-extension-frontmatter: 2.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + mdn-data@2.27.1: {} + + memoize-one@6.0.0: {} + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-frontmatter@2.0.0: + dependencies: + fault: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@2.6.0: {} + + mimic-fn@2.1.0: {} + + mimic-response@1.0.1: {} + + mimic-response@3.1.0: {} + + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.4 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.12 + + minimatch@5.1.9: + dependencies: + brace-expansion: 2.0.2 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + minipass-collect@1.0.2: + dependencies: + minipass: 3.3.6 + + minipass-fetch@2.1.2: + dependencies: + minipass: 3.3.6 + minipass-sized: 1.0.3 + minizlib: 2.1.2 + optionalDependencies: + encoding: 0.1.13 + + minipass-flush@1.0.5: + dependencies: + minipass: 3.3.6 + + minipass-pipeline@1.2.4: + dependencies: + minipass: 3.3.6 + + minipass-sized@1.0.3: + dependencies: + minipass: 3.3.6 + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + + minipass@7.1.3: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + mitt@3.0.1: {} + + mkdirp@1.0.4: {} + + mlly@1.8.1: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + + module-replacements@2.11.0: {} + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + natural-orderby@5.0.0: {} + + negotiator@0.6.4: {} + + node-abi@3.87.0: + dependencies: + semver: 7.7.4 + + node-addon-api@1.7.2: + optional: true + + node-addon-api@7.1.1: + optional: true + + node-api-version@0.2.1: + dependencies: + semver: 7.7.4 + + node-fetch-native@1.6.7: {} + + node-gyp@9.4.1: + dependencies: + env-paths: 2.2.1 + exponential-backoff: 3.1.3 + glob: 7.2.3 + graceful-fs: 4.2.11 + make-fetch-happen: 10.2.1 + nopt: 6.0.0 + npmlog: 6.0.2 + rimraf: 3.0.2 + semver: 7.7.4 + tar: 6.2.1 + which: 2.0.2 + transitivePeerDependencies: + - bluebird + - supports-color + + node-releases@2.0.36: {} + + nopt@6.0.0: + dependencies: + abbrev: 1.1.1 + + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + + normalize-path@3.0.0: {} + + normalize-url@6.1.0: {} + + normalize-wheel-es@1.2.0: {} + + npmlog@6.0.2: + dependencies: + are-we-there-yet: 3.0.1 + console-control-strings: 1.1.0 + gauge: 4.0.4 + set-blocking: 2.0.0 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + object-deep-merge@2.0.0: {} + + object-keys@1.1.1: + optional: true + + obug@2.1.1: {} + + ofetch@1.5.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.7 + ufo: 1.6.3 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + oxc-parser@0.115.0: + dependencies: + '@oxc-project/types': 0.115.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.115.0 + '@oxc-parser/binding-android-arm64': 0.115.0 + '@oxc-parser/binding-darwin-arm64': 0.115.0 + '@oxc-parser/binding-darwin-x64': 0.115.0 + '@oxc-parser/binding-freebsd-x64': 0.115.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.115.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.115.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.115.0 + '@oxc-parser/binding-linux-arm64-musl': 0.115.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.115.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.115.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.115.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.115.0 + '@oxc-parser/binding-linux-x64-gnu': 0.115.0 + '@oxc-parser/binding-linux-x64-musl': 0.115.0 + '@oxc-parser/binding-openharmony-arm64': 0.115.0 + '@oxc-parser/binding-wasm32-wasi': 0.115.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.115.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.115.0 + '@oxc-parser/binding-win32-x64-msvc': 0.115.0 + + oxc-walker@0.7.0(oxc-parser@0.115.0): + dependencies: + magic-regexp: 0.10.0 + oxc-parser: 0.115.0 + + p-cancelable@2.1.1: {} + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + + package-json-from-dist@1.0.1: {} + + package-manager-detector@1.6.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-gitignore@2.0.0: {} + + parse-imports-exports@0.2.4: + dependencies: + parse-statements: 1.0.11 + + parse-statements@1.0.11: {} + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + pathe@2.0.3: {} + + pe-library@0.4.1: {} + + pend@1.2.0: {} + + perfect-debounce@1.0.0: {} + + perfect-debounce@2.1.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pinia@3.0.4(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 7.7.9 + vue: 3.5.13(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.1 + pathe: 2.0.3 + + pkg-types@2.3.0: + dependencies: + confbox: 0.2.4 + exsolve: 1.0.8 + pathe: 2.0.3 + + playwright-core@1.58.2: {} + + playwright@1.58.2: + dependencies: + playwright-core: 1.58.2 + optionalDependencies: + fsevents: 2.3.2 + + plist@3.1.0: + dependencies: + '@xmldom/xmldom': 0.8.11 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + + pluralize@8.0.0: {} + + pnpm-workspace-yaml@1.6.0: + dependencies: + yaml: 2.8.2 + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + process-nextick-args@2.0.1: {} + + progress@2.0.3: {} + + promise-inflight@1.0.1: {} + + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + + proto-list@1.2.4: {} + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + punycode@2.3.1: {} + + quansync@0.2.11: {} + + quansync@1.0.0: {} + + quick-lru@5.1.1: {} + + read-binary-file-arch@1.0.6: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.9 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + readdirp@4.1.2: {} + + readdirp@5.0.0: {} + + refa@0.12.1: + dependencies: + '@eslint-community/regexpp': 4.12.2 + + regexp-ast-analysis@0.7.1: + dependencies: + '@eslint-community/regexpp': 4.12.2 + refa: 0.12.1 + + regexp-tree@0.1.27: {} + + regjsparser@0.13.0: + dependencies: + jsesc: 3.1.0 + + require-directory@2.1.1: {} + + resedit@1.7.2: + dependencies: + pe-library: 0.4.1 + + reserved-identifiers@1.2.0: {} + + resolve-alpn@1.2.1: {} + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + responselike@2.0.1: + dependencies: + lowercase-keys: 2.0.0 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + retry@0.12.0: {} + + rfdc@1.4.1: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + roarr@2.15.4: + dependencies: + boolean: 3.2.0 + detect-node: 2.1.0 + globalthis: 1.0.4 + json-stringify-safe: 5.0.1 + semver-compare: 1.0.0 + sprintf-js: 1.1.3 + optional: true + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + sanitize-filename@1.6.3: + dependencies: + truncate-utf8-bytes: 1.0.2 + + sass@1.97.3: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.5 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.6 + + sax@1.5.0: {} + + scslre@0.3.0: + dependencies: + '@eslint-community/regexpp': 4.12.2 + refa: 0.12.1 + regexp-ast-analysis: 0.7.1 + + scule@1.3.0: {} + + semver-compare@1.0.0: + optional: true + + semver@6.3.1: {} + + semver@7.7.4: {} + + serialize-error@7.0.1: + dependencies: + type-fest: 0.13.1 + optional: true + + set-blocking@2.0.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + simple-update-notifier@2.0.0: + dependencies: + semver: 7.7.4 + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + sisteransi@1.0.5: {} + + slice-ansi@3.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + optional: true + + smart-buffer@4.2.0: {} + + socks-proxy-agent@7.0.0: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + socks: 2.8.7 + transitivePeerDependencies: + - supports-color + + socks@2.8.7: + dependencies: + ip-address: 10.1.0 + smart-buffer: 4.2.0 + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@4.0.0: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.23 + + spdx-license-ids@3.0.23: {} + + speakingurl@14.0.1: {} + + sprintf-js@1.1.3: + optional: true + + ssri@9.0.1: + dependencies: + minipass: 3.3.6 + + stackback@0.0.2: {} + + stat-mode@1.0.0: {} + + std-env@4.0.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-indent@4.1.1: {} + + strip-json-comments@3.1.1: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + sumchecker@3.0.1: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + superjson@2.2.6: + dependencies: + copy-anything: 4.0.5 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + synckit@0.11.12: + dependencies: + '@pkgr/core': 0.2.9 + + tapable@2.3.0: {} + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + temp-file@3.4.0: + dependencies: + async-exit-hook: 2.0.1 + fs-extra: 10.1.0 + + tiny-typed-emitter@2.1.0: {} + + tinybench@2.9.0: {} + + tinyexec@1.0.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinyrainbow@3.1.0: {} + + tmp-promise@3.0.3: + dependencies: + tmp: 0.2.5 + + tmp@0.2.5: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + to-valid-identifier@1.0.0: + dependencies: + '@sindresorhus/base62': 1.0.0 + reserved-identifiers: 1.2.0 + + toml-eslint-parser@1.0.3: + dependencies: + eslint-visitor-keys: 5.0.1 + + totalist@3.0.1: {} + + truncate-utf8-bytes@1.0.2: + dependencies: + utf8-byte-length: 1.0.5 + + ts-api-utils@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-declaration-location@1.0.7(typescript@5.9.3): + dependencies: + picomatch: 4.0.3 + typescript: 5.9.3 + + tslib@2.8.1: + optional: true + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.13.1: + optional: true + + type-level-regexp@0.1.17: {} + + typescript@5.9.3: {} + + ufo@1.6.3: {} + + unconfig-core@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + quansync: 1.0.0 + + unconfig@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + defu: 6.1.4 + jiti: 2.6.1 + quansync: 1.0.0 + unconfig-core: 7.5.0 + + undici-types@6.21.0: {} + + unimport@4.2.0: + dependencies: + acorn: 8.16.0 + escape-string-regexp: 5.0.0 + estree-walker: 3.0.3 + local-pkg: 1.1.2 + magic-string: 0.30.21 + mlly: 1.8.1 + pathe: 2.0.3 + picomatch: 4.0.3 + pkg-types: 2.3.0 + scule: 1.3.0 + strip-literal: 3.1.0 + tinyglobby: 0.2.15 + unplugin: 2.3.11 + unplugin-utils: 0.2.5 + + unique-filename@2.0.1: + dependencies: + unique-slug: 3.0.0 + + unique-slug@3.0.0: + dependencies: + imurmurhash: 0.1.4 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + universalify@0.1.2: {} + + universalify@2.0.1: {} + + unocss-preset-chinese@0.3.3(unocss@66.6.6(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2))): + dependencies: + '@unocss/core': 0.62.4 + '@unocss/preset-mini': 0.62.4 + optionalDependencies: + unocss: 66.6.6(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2)) + + unocss-preset-ease@0.0.4(unocss@66.6.6(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2))): + optionalDependencies: + unocss: 66.6.6(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2)) + + unocss@66.6.6(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2)): + dependencies: + '@unocss/cli': 66.6.6 + '@unocss/core': 66.6.6 + '@unocss/preset-attributify': 66.6.6 + '@unocss/preset-icons': 66.6.6 + '@unocss/preset-mini': 66.6.6 + '@unocss/preset-tagify': 66.6.6 + '@unocss/preset-typography': 66.6.6 + '@unocss/preset-uno': 66.6.6 + '@unocss/preset-web-fonts': 66.6.6 + '@unocss/preset-wind': 66.6.6 + '@unocss/preset-wind3': 66.6.6 + '@unocss/preset-wind4': 66.6.6 + '@unocss/transformer-attributify-jsx': 66.6.6 + '@unocss/transformer-compile-class': 66.6.6 + '@unocss/transformer-directives': 66.6.6 + '@unocss/transformer-variant-group': 66.6.6 + '@unocss/vite': 66.6.6(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2)) + transitivePeerDependencies: + - vite + + unplugin-auto-import@19.3.0(@vueuse/core@12.8.2(typescript@5.9.3)): + dependencies: + local-pkg: 1.1.2 + magic-string: 0.30.21 + picomatch: 4.0.3 + unimport: 4.2.0 + unplugin: 2.3.11 + unplugin-utils: 0.2.5 + optionalDependencies: + '@vueuse/core': 12.8.2(typescript@5.9.3) + + unplugin-utils@0.2.5: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.3 + + unplugin-utils@0.3.1: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.3 + + unplugin-vue-components@28.8.0(@babel/parser@7.29.0)(vue@3.5.13(typescript@5.9.3)): + dependencies: + chokidar: 3.6.0 + debug: 4.4.3 + local-pkg: 1.1.2 + magic-string: 0.30.21 + mlly: 1.8.1 + tinyglobby: 0.2.15 + unplugin: 2.3.11 + unplugin-utils: 0.2.5 + vue: 3.5.13(typescript@5.9.3) + optionalDependencies: + '@babel/parser': 7.29.0 + transitivePeerDependencies: + - supports-color + + unplugin@2.3.11: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.16.0 + picomatch: 4.0.3 + webpack-virtual-modules: 0.6.2 + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + utf8-byte-length@1.0.5: {} + + util-deprecate@1.0.2: {} + + verror@1.10.1: + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.4.1 + optional: true + + vite@5.4.11(@types/node@22.19.15)(sass@1.97.3): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.8 + rollup: 4.59.0 + optionalDependencies: + '@types/node': 22.19.15 + fsevents: 2.3.3 + sass: 1.97.3 + + vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.8 + rollup: 4.59.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 22.19.15 + fsevents: 2.3.3 + jiti: 2.6.1 + sass: 1.97.3 + yaml: 2.8.2 + + vitest@4.1.0(@types/node@22.19.15)(happy-dom@20.8.4)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2)): + dependencies: + '@vitest/expect': 4.1.0 + '@vitest/mocker': 4.1.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2)) + '@vitest/pretty-format': 4.1.0 + '@vitest/runner': 4.1.0 + '@vitest/snapshot': 4.1.0 + '@vitest/spy': 4.1.0 + '@vitest/utils': 4.1.0 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.15 + happy-dom: 20.8.4 + transitivePeerDependencies: + - msw + + vscode-uri@3.1.0: {} + + vue-component-type-helpers@2.2.12: {} + + vue-eslint-parser@10.4.0(eslint@9.39.4(jiti@2.6.1)): + dependencies: + debug: 4.4.3 + eslint: 9.39.4(jiti@2.6.1) + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + semver: 7.7.4 + transitivePeerDependencies: + - supports-color + + vue-router@4.6.4(vue@3.5.13(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.13(typescript@5.9.3) + + vue-tsc@2.2.0(typescript@5.9.3): + dependencies: + '@volar/typescript': 2.4.28 + '@vue/language-core': 2.2.0(typescript@5.9.3) + typescript: 5.9.3 + + vue@3.5.13(typescript@5.9.3): + dependencies: + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-sfc': 3.5.13 + '@vue/runtime-dom': 3.5.13 + '@vue/server-renderer': 3.5.13(vue@3.5.13(typescript@5.9.3)) + '@vue/shared': 3.5.13 + optionalDependencies: + typescript: 5.9.3 + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + webpack-virtual-modules@0.6.2: {} + + whatwg-mimetype@3.0.0: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + wrappy@1.0.2: {} + + ws@8.19.0: {} + + xml-name-validator@4.0.0: {} + + xmlbuilder@15.1.1: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yallist@4.0.0: {} + + yaml-eslint-parser@2.0.0: + dependencies: + eslint-visitor-keys: 5.0.1 + yaml: 2.8.2 + + yaml@2.8.2: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + + yocto-queue@0.1.0: {} + + zip-stream@4.1.1: + dependencies: + archiver-utils: 3.0.4 + compress-commons: 4.1.2 + readable-stream: 3.6.2 + + zod@3.25.76: {} + + zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..533016a --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,7 @@ +shellEmulator: true + +trustPolicy: no-downgrade + +packages: + - 'apps/*' + - 'packages/*' diff --git a/references/中控--DCS资料/ECS-700使用说明/1.jpg b/references/中控--DCS资料/ECS-700使用说明/1.jpg new file mode 100755 index 0000000..e47510d Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/1.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/10.jpg b/references/中控--DCS资料/ECS-700使用说明/10.jpg new file mode 100755 index 0000000..98311fc Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/10.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/11.jpg b/references/中控--DCS资料/ECS-700使用说明/11.jpg new file mode 100755 index 0000000..034310f Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/11.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/12.jpg b/references/中控--DCS资料/ECS-700使用说明/12.jpg new file mode 100755 index 0000000..180bdf4 Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/12.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/13.jpg b/references/中控--DCS资料/ECS-700使用说明/13.jpg new file mode 100755 index 0000000..55e431e Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/13.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/14.jpg b/references/中控--DCS资料/ECS-700使用说明/14.jpg new file mode 100755 index 0000000..26fd964 Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/14.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/15.jpg b/references/中控--DCS资料/ECS-700使用说明/15.jpg new file mode 100755 index 0000000..f92f780 Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/15.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/16.jpg b/references/中控--DCS资料/ECS-700使用说明/16.jpg new file mode 100755 index 0000000..cab449e Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/16.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/17.jpg b/references/中控--DCS资料/ECS-700使用说明/17.jpg new file mode 100755 index 0000000..8551066 Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/17.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/18.jpg b/references/中控--DCS资料/ECS-700使用说明/18.jpg new file mode 100755 index 0000000..99d3e8a Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/18.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/19.jpg b/references/中控--DCS资料/ECS-700使用说明/19.jpg new file mode 100755 index 0000000..f46fa14 Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/19.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/2.jpg b/references/中控--DCS资料/ECS-700使用说明/2.jpg new file mode 100755 index 0000000..bf0e58c Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/2.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/20.jpg b/references/中控--DCS资料/ECS-700使用说明/20.jpg new file mode 100755 index 0000000..bb400cc Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/20.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/21.jpg b/references/中控--DCS资料/ECS-700使用说明/21.jpg new file mode 100755 index 0000000..ef5e41c Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/21.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/22.jpg b/references/中控--DCS资料/ECS-700使用说明/22.jpg new file mode 100755 index 0000000..7fe2f97 Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/22.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/23.jpg b/references/中控--DCS资料/ECS-700使用说明/23.jpg new file mode 100755 index 0000000..8cde752 Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/23.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/24.jpg b/references/中控--DCS资料/ECS-700使用说明/24.jpg new file mode 100755 index 0000000..a78f27c Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/24.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/25.jpg b/references/中控--DCS资料/ECS-700使用说明/25.jpg new file mode 100755 index 0000000..babdac5 Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/25.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/26.jpg b/references/中控--DCS资料/ECS-700使用说明/26.jpg new file mode 100755 index 0000000..572ab43 Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/26.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/27.jpg b/references/中控--DCS资料/ECS-700使用说明/27.jpg new file mode 100755 index 0000000..c200b2b Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/27.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/28.jpg b/references/中控--DCS资料/ECS-700使用说明/28.jpg new file mode 100755 index 0000000..7b7b66d Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/28.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/29.jpg b/references/中控--DCS资料/ECS-700使用说明/29.jpg new file mode 100755 index 0000000..531a431 Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/29.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/3.jpg b/references/中控--DCS资料/ECS-700使用说明/3.jpg new file mode 100755 index 0000000..bc53c5d Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/3.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/30.jpg b/references/中控--DCS资料/ECS-700使用说明/30.jpg new file mode 100755 index 0000000..ed8e608 Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/30.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/31.jpg b/references/中控--DCS资料/ECS-700使用说明/31.jpg new file mode 100755 index 0000000..e1e29b0 Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/31.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/32.jpg b/references/中控--DCS资料/ECS-700使用说明/32.jpg new file mode 100755 index 0000000..0f2ee92 Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/32.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/33.jpg b/references/中控--DCS资料/ECS-700使用说明/33.jpg new file mode 100755 index 0000000..0050980 Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/33.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/34.jpg b/references/中控--DCS资料/ECS-700使用说明/34.jpg new file mode 100755 index 0000000..4132125 Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/34.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/35.jpg b/references/中控--DCS资料/ECS-700使用说明/35.jpg new file mode 100755 index 0000000..86444f0 Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/35.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/4.jpg b/references/中控--DCS资料/ECS-700使用说明/4.jpg new file mode 100755 index 0000000..feec108 Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/4.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/5.jpg b/references/中控--DCS资料/ECS-700使用说明/5.jpg new file mode 100755 index 0000000..5e52cab Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/5.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/6.jpg b/references/中控--DCS资料/ECS-700使用说明/6.jpg new file mode 100755 index 0000000..987d3f8 Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/6.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/7.jpg b/references/中控--DCS资料/ECS-700使用说明/7.jpg new file mode 100755 index 0000000..36f3c1a Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/7.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/8.jpg b/references/中控--DCS资料/ECS-700使用说明/8.jpg new file mode 100755 index 0000000..413ab6c Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/8.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/9.jpg b/references/中控--DCS资料/ECS-700使用说明/9.jpg new file mode 100755 index 0000000..69198b8 Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/9.jpg differ diff --git a/references/中控--DCS资料/ECS-700使用说明/ECS-700操作指导.doc b/references/中控--DCS资料/ECS-700使用说明/ECS-700操作指导.doc new file mode 100755 index 0000000..2ab8618 Binary files /dev/null and b/references/中控--DCS资料/ECS-700使用说明/ECS-700操作指导.doc differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/0.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/0.jpg new file mode 100755 index 0000000..28fed2a Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/0.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/10.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/10.jpg new file mode 100755 index 0000000..9fa3690 Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/10.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/11.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/11.jpg new file mode 100755 index 0000000..7e9dc0e Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/11.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/12.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/12.jpg new file mode 100755 index 0000000..8d23fae Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/12.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/13.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/13.jpg new file mode 100755 index 0000000..e036546 Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/13.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/14.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/14.jpg new file mode 100755 index 0000000..54b6b0d Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/14.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/15.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/15.jpg new file mode 100755 index 0000000..43c1a91 Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/15.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/16.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/16.jpg new file mode 100755 index 0000000..12ff949 Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/16.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/17.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/17.jpg new file mode 100755 index 0000000..cafdbd9 Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/17.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/18.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/18.jpg new file mode 100755 index 0000000..56b974f Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/18.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/19.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/19.jpg new file mode 100755 index 0000000..e9c1a8b Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/19.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/2.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/2.jpg new file mode 100755 index 0000000..aea5072 Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/2.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/20.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/20.jpg new file mode 100755 index 0000000..ca5d39b Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/20.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/21.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/21.jpg new file mode 100755 index 0000000..1bc1550 Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/21.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/22.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/22.jpg new file mode 100755 index 0000000..78ae403 Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/22.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/23.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/23.jpg new file mode 100755 index 0000000..a09d9ae Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/23.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/24.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/24.jpg new file mode 100755 index 0000000..054a729 Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/24.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/25.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/25.jpg new file mode 100755 index 0000000..aada341 Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/25.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/26.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/26.jpg new file mode 100755 index 0000000..d1de05e Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/26.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/27.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/27.jpg new file mode 100755 index 0000000..a5dd461 Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/27.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/28.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/28.jpg new file mode 100755 index 0000000..ae2c90a Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/28.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/29.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/29.jpg new file mode 100755 index 0000000..b219687 Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/29.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/3.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/3.jpg new file mode 100755 index 0000000..30bd2f6 Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/3.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/30.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/30.jpg new file mode 100755 index 0000000..5c25660 Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/30.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/31.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/31.jpg new file mode 100755 index 0000000..1fe905c Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/31.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/4.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/4.jpg new file mode 100755 index 0000000..a12397f Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/4.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/5.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/5.jpg new file mode 100755 index 0000000..2de0827 Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/5.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/6.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/6.jpg new file mode 100755 index 0000000..10dad14 Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/6.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/7.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/7.jpg new file mode 100755 index 0000000..ba020ad Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/7.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/8.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/8.jpg new file mode 100755 index 0000000..e0b8c3c Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/8.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作手册(通用)/9.jpg b/references/中控--DCS资料/ECS-700操作手册(通用)/9.jpg new file mode 100755 index 0000000..5bf2ee7 Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作手册(通用)/9.jpg differ diff --git a/references/中控--DCS资料/ECS-700操作指导 V1.1.pdf b/references/中控--DCS资料/ECS-700操作指导 V1.1.pdf new file mode 100644 index 0000000..146a447 Binary files /dev/null and b/references/中控--DCS资料/ECS-700操作指导 V1.1.pdf differ diff --git a/references/中控--DCS资料/FF实时监控软件使用手册.pdf b/references/中控--DCS资料/FF实时监控软件使用手册.pdf new file mode 100755 index 0000000..091d227 Binary files /dev/null and b/references/中控--DCS资料/FF实时监控软件使用手册.pdf differ diff --git a/references/中控--DCS资料/实时监控软件使用手册.pdf b/references/中控--DCS资料/实时监控软件使用手册.pdf new file mode 100755 index 0000000..fe5bff4 Binary files /dev/null and b/references/中控--DCS资料/实时监控软件使用手册.pdf differ diff --git a/references/中控--DCS资料/操作员指导手册.pdf b/references/中控--DCS资料/操作员指导手册.pdf new file mode 100644 index 0000000..def9ee3 Binary files /dev/null and b/references/中控--DCS资料/操作员指导手册.pdf differ diff --git a/references/中控--DCS资料/操作指导手册.pdf b/references/中控--DCS资料/操作指导手册.pdf new file mode 100755 index 0000000..81ce721 Binary files /dev/null and b/references/中控--DCS资料/操作指导手册.pdf differ diff --git a/references/中控--DCS资料/数据应用分析软件使用手册.pdf b/references/中控--DCS资料/数据应用分析软件使用手册.pdf new file mode 100755 index 0000000..08adc72 Binary files /dev/null and b/references/中控--DCS资料/数据应用分析软件使用手册.pdf differ diff --git a/references/中控--DCS资料/有机分厂合成工序工艺安全操作规程正文(20121110)(1).pdf b/references/中控--DCS资料/有机分厂合成工序工艺安全操作规程正文(20121110)(1).pdf new file mode 100755 index 0000000..70ac9e7 Binary files /dev/null and b/references/中控--DCS资料/有机分厂合成工序工艺安全操作规程正文(20121110)(1).pdf differ diff --git a/references/中控--DCS资料/浙江中控介绍.mp4 b/references/中控--DCS资料/浙江中控介绍.mp4 new file mode 100755 index 0000000..ac817da Binary files /dev/null and b/references/中控--DCS资料/浙江中控介绍.mp4 differ diff --git a/references/竞品部分截图/弹窗1.png b/references/竞品部分截图/弹窗1.png new file mode 100644 index 0000000..ebfad1f Binary files /dev/null and b/references/竞品部分截图/弹窗1.png differ diff --git a/references/竞品部分截图/弹窗2.png b/references/竞品部分截图/弹窗2.png new file mode 100644 index 0000000..097577d Binary files /dev/null and b/references/竞品部分截图/弹窗2.png differ diff --git a/references/竞品部分截图/编辑器.png b/references/竞品部分截图/编辑器.png new file mode 100644 index 0000000..25bed24 Binary files /dev/null and b/references/竞品部分截图/编辑器.png differ diff --git a/references/竞品部分截图/运行.png b/references/竞品部分截图/运行.png new file mode 100644 index 0000000..18676b7 Binary files /dev/null and b/references/竞品部分截图/运行.png differ diff --git a/tests/e2e/canvas-editor.spec.ts b/tests/e2e/canvas-editor.spec.ts new file mode 100644 index 0000000..9bd227c --- /dev/null +++ b/tests/e2e/canvas-editor.spec.ts @@ -0,0 +1,181 @@ +import { expect, test } from '@playwright/test' + +/** + * DCS 编辑器 - 画布编辑器核心功能测试 + * + * 测试画布页面管理、侧边栏标签切换、组件面板等核心交互。 + * 每个测试独立运行,通过创建画布进入编辑器。 + */ + +/** 辅助函数:创建画布并进入编辑器 */ +async function enterEditor(page: import('@playwright/test').Page, canvasName = '测试画布') { + await page.goto('/') + await page.waitForLoadState('networkidle') + await page.locator('.add-card').click() + const dialog = page.locator('.el-dialog') + await expect(dialog).toBeVisible({ timeout: 5000 }) + await dialog.locator('input').first().fill(canvasName) + await dialog.locator('button').filter({ hasText: '创建' }).click() + await expect(page.locator('.dcs-editor')).toBeVisible({ timeout: 10000 }) +} + +test.describe('画布编辑器核心功能', () => { + test('编辑器加载后显示画布舞台', async ({ page }) => { + await enterEditor(page, '画布加载测试') + + // 画布舞台(wrapper)应该可见 + await expect(page.locator('.canvas-wrapper')).toBeVisible() + // 画布内 canvas 元素应存在 + await expect(page.locator('.canvas-wrapper canvas')).toBeVisible() + // 标尺应该可见 + await expect(page.locator('.ruler-top')).toBeVisible() + await expect(page.locator('.ruler-left')).toBeVisible() + }) + + test('侧边栏标签 - 默认显示图层面板', async ({ page }) => { + await enterEditor(page, '侧边栏标签测试') + + // 默认激活的标签应该是"图层" + const activeTab = page.locator('.sidebar-tab-item.active') + await expect(activeTab).toContainText('图层') + + // 图层面板区域应该可见 + await expect(page.locator('.layers-tab')).toBeVisible() + }) + + test('侧边栏标签 - 切换到组件面板', async ({ page }) => { + await enterEditor(page, '组件面板测试') + + // 点击"组件"标签 + await page.locator('.sidebar-tab-item').filter({ hasText: '组件' }).click() + + // 组件标签应变为激活状态 + const activeTab = page.locator('.sidebar-tab-item.active') + await expect(activeTab).toContainText('组件') + + // 组件面板应该可见,且包含提示文字 + await expect(page.locator('.components-tab')).toBeVisible() + await expect(page.locator('.components-hint')).toContainText('拖拽添加到画布') + }) + + test('侧边栏标签 - 切换到模板面板', async ({ page }) => { + await enterEditor(page, '模板面板测试') + + // 点击"模板"标签 + await page.locator('.sidebar-tab-item').filter({ hasText: '模板' }).click() + + // 模板标签应变为激活状态 + const activeTab = page.locator('.sidebar-tab-item.active') + await expect(activeTab).toContainText('模板') + }) + + test('侧边栏标签 - 可以依次切换回图层面板', async ({ page }) => { + await enterEditor(page, '标签回切测试') + + // 先切到组件 + await page.locator('.sidebar-tab-item').filter({ hasText: '组件' }).click() + await expect(page.locator('.sidebar-tab-item.active')).toContainText('组件') + + // 再切回图层 + await page.locator('.sidebar-tab-item').filter({ hasText: '图层' }).click() + await expect(page.locator('.sidebar-tab-item.active')).toContainText('图层') + await expect(page.locator('.layers-tab')).toBeVisible() + }) + + test('组件面板显示所有可用组件', async ({ page }) => { + await enterEditor(page, '组件列表测试') + + // 切换到组件面板 + await page.locator('.sidebar-tab-item').filter({ hasText: '组件' }).click() + await expect(page.locator('.components-tab')).toBeVisible() + + // 验证组件卡片数量(constants.ts 定义了 5 个:矩形、数值、文本、棒图、按钮) + const cards = page.locator('.component-card') + await expect(cards).toHaveCount(5) + + // 验证各组件名称 + await expect(page.locator('.card-title').filter({ hasText: '矩形' })).toBeVisible() + await expect(page.locator('.card-title').filter({ hasText: '数值' })).toBeVisible() + await expect(page.locator('.card-title').filter({ hasText: '文本' })).toBeVisible() + await expect(page.locator('.card-title').filter({ hasText: '棒图' })).toBeVisible() + await expect(page.locator('.card-title').filter({ hasText: '按钮' })).toBeVisible() + }) + + test('图层面板显示页面列表和画布页数', async ({ page }) => { + await enterEditor(page, '页面列表测试') + + // 图层面板应可见 + await expect(page.locator('.layers-tab')).toBeVisible() + + // 页面头部应显示页数 + await expect(page.locator('.page-header')).toContainText('页数') + + // 页面列表应至少有一个活动页面 + const activePage = page.locator('.page-row.active') + await expect(activePage).toBeVisible() + }) + + test('点击画布背景可以取消图层选中', async ({ page }) => { + await enterEditor(page, '取消选中测试') + + // 点击画布背景区域 + await page.locator('.canvas-wrapper').click({ position: { x: 50, y: 50 } }) + + // 状态栏不应显示选中信息(没有选中任何图层时不会显示"已选中") + await expect(page.locator('.editor-status-bar .status-right')).not.toContainText('已选中') + }) + + test('撤销按钮初始状态为禁用', async ({ page }) => { + await enterEditor(page, '撤销状态测试') + + // 撤销按钮初始应该是禁用的(没有操作历史) + const undoBtn = page.locator('.editor-header .icon-btn').filter({ has: page.locator('.fa-rotate-left') }) + await expect(undoBtn).toBeDisabled() + + // 重做按钮初始也应该是禁用的 + const redoBtn = page.locator('.editor-header .icon-btn').filter({ has: page.locator('.fa-rotate-right') }) + await expect(redoBtn).toBeDisabled() + }) + + test('头部包含分享和运行按钮', async ({ page }) => { + await enterEditor(page, '头部按钮测试') + + // 分享按钮 + await expect(page.locator('.share-btn')).toBeVisible() + await expect(page.locator('.share-btn')).toContainText('分享') + + // 运行组合按钮 + await expect(page.locator('.run-combo-btn')).toBeVisible() + await expect(page.locator('.run-main-btn')).toBeVisible() + }) + + test('工具栏显示工具按钮', async ({ page }) => { + await enterEditor(page, '工具栏测试') + + // 工具栏应可见 + await expect(page.locator('.tool-rail')).toBeVisible() + + // 工具按钮应存在(至少有移动工具、文本工具、矩形工具 + 底部折叠按钮) + const toolButtons = page.locator('.tool-rail .tool-btn') + const count = await toolButtons.count() + expect(count).toBeGreaterThanOrEqual(3) + }) + + test('侧边栏可以折叠和展开', async ({ page }) => { + await enterEditor(page, '折叠展开测试') + + // 确认侧边栏面板初始可见 + await expect(page.locator('.editor-sidebar-panel')).toBeVisible() + + // 点击折叠按钮(工具栏底部的按钮,有 fa-angles-left 图标) + const collapseBtn = page.locator('.tool-bottom .tool-btn') + await collapseBtn.click() + + // 侧边栏应添加 is-collapsed 类 + await expect(page.locator('.editor-sidebar-shell.is-collapsed')).toBeVisible() + + // 再次点击展开 + await collapseBtn.click() + await expect(page.locator('.editor-sidebar-shell:not(.is-collapsed)')).toBeVisible() + }) +}) diff --git a/tests/e2e/layer-operations.spec.ts b/tests/e2e/layer-operations.spec.ts new file mode 100644 index 0000000..f12a996 --- /dev/null +++ b/tests/e2e/layer-operations.spec.ts @@ -0,0 +1,223 @@ +import { expect, test } from '@playwright/test' + +/** + * DCS 编辑器 - 图层操作测试 + * + * 测试图层的添加、选择、删除等操作。 + * 部分测试(如拖拽添加组件)因 Playwright 对 HTML5 drag-and-drop 的限制 + * 而标记为 skip,仅验证可测试的交互路径。 + */ + +/** 辅助函数:创建画布并进入编辑器 */ +async function enterEditor(page: import('@playwright/test').Page, canvasName = '图层测试画布') { + await page.goto('/') + await page.waitForLoadState('networkidle') + await page.locator('.add-card').click() + const dialog = page.locator('.el-dialog') + await expect(dialog).toBeVisible({ timeout: 5000 }) + await dialog.locator('input').first().fill(canvasName) + await dialog.locator('button').filter({ hasText: '创建' }).click() + await expect(page.locator('.dcs-editor')).toBeVisible({ timeout: 10000 }) +} + +/** 辅助函数:获取图层面板中的图层行数量 */ +async function getLayerRowCount(page: import('@playwright/test').Page): Promise { + return page.locator('.layer-list .layer-row').count() +} + +test.describe('图层管理', () => { + test.skip('通过拖拽添加组件到画布', async ({ page }) => { + // 跳过:HTML5 drag-and-drop 在 Playwright 中较难模拟, + // 且应用在 Tauri 环境还使用了 pointer 方案替代。 + await enterEditor(page, '拖拽添加测试') + + // 切换到组件面板 + await page.locator('.sidebar-tab-item').filter({ hasText: '组件' }).click() + await expect(page.locator('.components-tab')).toBeVisible() + + // 获取矩形组件卡片和画布舞台区域 + const rectCard = page.locator('.component-card').first() + const canvas = page.locator('.canvas-wrapper') + + // 尝试拖拽(此操作可能不可靠) + await rectCard.dragTo(canvas, { + targetPosition: { x: 300, y: 300 }, + }) + + // 验证图层面板中出现新图层 + await page.locator('.sidebar-tab-item').filter({ hasText: '图层' }).click() + const layerCount = await getLayerRowCount(page) + expect(layerCount).toBeGreaterThan(0) + }) + + test('初始状态下图层面板为空', async ({ page }) => { + await enterEditor(page, '空图层面板测试') + + // 确认图层面板可见 + await expect(page.locator('.layers-tab')).toBeVisible() + + // 新画布应该没有图层 + const layerCount = await getLayerRowCount(page) + expect(layerCount).toBe(0) + }) + + test('属性面板初始显示空状态或底图信息', async ({ page }) => { + await enterEditor(page, '属性面板初始测试') + + // 属性面板头部应该可见且显示"属性"标题 + const propertyTitle = page.locator('.property-panel .title') + await expect(propertyTitle).toContainText('属性') + + // 没有选中图层时,应显示提示文字 + const summaryOrEmpty = page.locator('.property-panel .selection-summary, .property-panel .property-empty') + await expect(summaryOrEmpty.first()).toBeVisible() + }) + + test('属性面板有停靠/浮动切换按钮', async ({ page }) => { + await enterEditor(page, '停靠切换测试') + + // 停靠/浮动切换按钮应存在 + const dockBtn = page.locator('.dock-toggle-btn') + await expect(dockBtn).toBeVisible() + + // 点击切换到浮动模式 + await dockBtn.click() + await expect(page.locator('.property-panel-shell--floating')).toBeVisible() + + // 再次点击切换回停靠模式 + await page.locator('.dock-toggle-btn').click() + await expect(page.locator('.property-panel-shell:not(.property-panel-shell--floating)')).toBeVisible() + }) + + test('图层面板显示页面列表', async ({ page }) => { + await enterEditor(page, '页面列表验证测试') + + // 页面树区域应显示至少一个页面 + const pageRows = page.locator('.page-tree .page-row') + await expect(pageRows.first()).toBeVisible() + + // 第一个页面应处于激活状态 + await expect(page.locator('.page-row.active')).toBeVisible() + }) + + test('图层面板有添加页面按钮', async ({ page }) => { + await enterEditor(page, '添加页面按钮测试') + + // 页面头部的添加按钮应存在(fa-plus 图标) + const addPageBtn = page.locator('.page-header').locator('button').filter({ + has: page.locator('.fa-plus'), + }) + await expect(addPageBtn).toBeVisible() + }) + + test('图层面板有搜索框', async ({ page }) => { + await enterEditor(page, '图层搜索测试') + + // 搜索行应包含输入框 + const searchInput = page.locator('.layer-search-row input') + await expect(searchInput).toBeVisible() + }) + + test('状态栏无选中时不显示选中计数', async ({ page }) => { + await enterEditor(page, '状态栏选中计数测试') + + // 没有选中图层时,状态栏右侧不应显示"已选中" + await expect(page.locator('.editor-status-bar .status-right')).not.toContainText('已选中') + }) + + test('缩放信息显示在状态栏中', async ({ page }) => { + await enterEditor(page, '状态栏缩放测试') + + // 状态栏应显示缩放百分比(如"100%") + const statusItems = page.locator('.editor-status-bar .status-item') + let foundZoom = false + const count = await statusItems.count() + for (let i = 0; i < count; i++) { + const text = await statusItems.nth(i).textContent() + if (text && text.includes('%')) { + foundZoom = true + break + } + } + expect(foundZoom).toBe(true) + }) + + test('画布舞台有标尺', async ({ page }) => { + await enterEditor(page, '标尺测试') + + // 水平标尺 + await expect(page.locator('.ruler-top')).toBeVisible() + // 垂直标尺 + await expect(page.locator('.ruler-left')).toBeVisible() + // 标尺角落 + await expect(page.locator('.ruler-corner')).toBeVisible() + }) + + test.skip('选中图层后属性面板显示属性', async ({ page }) => { + // 跳过:需要先有图层才能选中,添加图层依赖拖拽操作 + await enterEditor(page, '选中属性测试') + + // 预期:选中图层后,属性面板标题下方应显示图层信息 + const propertyBody = page.locator('.property-panel .property-body') + await expect(propertyBody).toBeVisible() + }) + + test.skip('可以删除选中的图层', async ({ page }) => { + // 跳过:需要先有可选中的图层(添加图层依赖拖拽操作) + await enterEditor(page, '删除图层测试') + + // 预期:选中图层后按 Delete/Backspace 可删除 + // 或通过右键菜单删除 + }) + + test.skip('删除图层后可撤销', async ({ page }) => { + // 跳过:依赖先添加并删除图层 + await enterEditor(page, '撤销删除测试') + + // 预期:删除图层后,撤销按钮变为可用,点击撤销可恢复图层 + }) + + test('图层面板页面列表可以折叠和展开', async ({ page }) => { + await enterEditor(page, '页面折叠测试') + + // 页面树应该初始可见 + await expect(page.locator('.page-tree')).toBeVisible() + + // 点击折叠/展开按钮(page-header 中的角标按钮) + const toggleBtn = page.locator('.page-header .header-actions button').filter({ + has: page.locator('.fa-angle-up, .fa-angle-down'), + }) + await toggleBtn.click() + + // 页面树应该被隐藏(v-show 控制,元素仍在 DOM 中但不可见) + await expect(page.locator('.page-tree')).toBeHidden() + + // 再次点击展开 + await toggleBtn.click() + await expect(page.locator('.page-tree')).toBeVisible() + }) + + test('画布舞台 overlay 层存在', async ({ page }) => { + await enterEditor(page, 'Overlay 测试') + + // overlay 是放置图层选择框等交互元素的层 + await expect(page.locator('.canvas-wrapper .overlay')).toBeAttached() + }) + + test('组件面板中的组件卡片显示描述信息', async ({ page }) => { + await enterEditor(page, '组件描述测试') + + // 切换到组件面板 + await page.locator('.sidebar-tab-item').filter({ hasText: '组件' }).click() + await expect(page.locator('.components-tab')).toBeVisible() + + // 每个组件卡片应该有标题和描述 + const firstCard = page.locator('.component-card').first() + await expect(firstCard.locator('.card-title')).toBeVisible() + await expect(firstCard.locator('.card-desc')).toBeVisible() + + // 验证具体描述内容(矩形:基础矩形图层) + const rectDesc = page.locator('.component-card').filter({ hasText: '矩形' }).locator('.card-desc') + await expect(rectDesc).toContainText('基础矩形图层') + }) +}) diff --git a/tests/e2e/navigation.spec.ts b/tests/e2e/navigation.spec.ts new file mode 100644 index 0000000..377f730 --- /dev/null +++ b/tests/e2e/navigation.spec.ts @@ -0,0 +1,138 @@ +import { expect, test } from '@playwright/test' + +/** + * DCS 编辑器 - 基础导航测试 + * + * 应用使用 Hash 路由(createWebHashHistory('/dcs-web')), + * 首次访问 Web 端默认跳转到 /canvases 画布列表页。 + */ + +test.describe('基础导航', () => { + test('应用加载无错误', async ({ page }) => { + const errors: string[] = [] + page.on('pageerror', (err) => { + errors.push(err.message) + }) + + await page.goto('/') + // 等待应用框架渲染完毕(路由守卫会重定向到 canvases 或 editor) + await page.waitForLoadState('networkidle') + + // 不应产生未捕获的 JS 异常 + expect(errors).toEqual([]) + }) + + test('可以导航到画布列表页', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + + // Web 模式下默认重定向到画布列表 + await expect(page.locator('.canvases-page')).toBeVisible({ timeout: 10000 }) + // 页面标题区域应包含 "DCS图" + await expect(page.locator('.page-title')).toContainText('DCS图') + // 应有"添加DCS图"卡片 + await expect(page.locator('.add-card')).toBeVisible() + }) + + test('可以导航到编辑器页面', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + + // 通过"添加DCS图"创建一个画布并进入编辑器 + await page.locator('.add-card').click() + // 等待创建对话框出现 + const dialog = page.locator('.el-dialog') + await expect(dialog).toBeVisible({ timeout: 5000 }) + + // 填写画布名称 + await dialog.locator('input').first().fill('测试画布') + // 点击创建按钮 + await dialog.locator('button').filter({ hasText: '创建' }).click() + + // 等待跳转到编辑器页面 + await expect(page.locator('.dcs-editor')).toBeVisible({ timeout: 10000 }) + }) + + test('编辑器包含侧边栏、画布舞台和属性面板', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + + // 先创建画布进入编辑器 + await page.locator('.add-card').click() + const dialog = page.locator('.el-dialog') + await expect(dialog).toBeVisible({ timeout: 5000 }) + await dialog.locator('input').first().fill('结构测试画布') + await dialog.locator('button').filter({ hasText: '创建' }).click() + await expect(page.locator('.dcs-editor')).toBeVisible({ timeout: 10000 }) + + // 验证编辑器三大区域 + // 1. 侧边栏(工具栏 + 面板) + await expect(page.locator('.editor-sidebar-shell')).toBeVisible() + await expect(page.locator('.tool-rail')).toBeVisible() + + // 2. 画布舞台 + await expect(page.locator('.canvas-wrapper')).toBeVisible() + + // 3. 属性面板 + await expect(page.locator('.property-panel-shell')).toBeVisible() + }) + + test('编辑器头部可见且包含撤销重做按钮', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + + // 创建画布进入编辑器 + await page.locator('.add-card').click() + const dialog = page.locator('.el-dialog') + await expect(dialog).toBeVisible({ timeout: 5000 }) + await dialog.locator('input').first().fill('头部测试画布') + await dialog.locator('button').filter({ hasText: '创建' }).click() + await expect(page.locator('.dcs-editor')).toBeVisible({ timeout: 10000 }) + + // 编辑器头部 + await expect(page.locator('.editor-header')).toBeVisible() + + // 撤销按钮(fa-rotate-left 图标) + const undoBtn = page.locator('.editor-header .icon-btn').filter({ has: page.locator('.fa-rotate-left') }) + await expect(undoBtn).toBeVisible() + + // 重做按钮(fa-rotate-right 图标) + const redoBtn = page.locator('.editor-header .icon-btn').filter({ has: page.locator('.fa-rotate-right') }) + await expect(redoBtn).toBeVisible() + }) + + test('状态栏在编辑器中可见', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + + // 创建画布进入编辑器 + await page.locator('.add-card').click() + const dialog = page.locator('.el-dialog') + await expect(dialog).toBeVisible({ timeout: 5000 }) + await dialog.locator('input').first().fill('状态栏测试画布') + await dialog.locator('button').filter({ hasText: '创建' }).click() + await expect(page.locator('.dcs-editor')).toBeVisible({ timeout: 10000 }) + + // 状态栏 + await expect(page.locator('.editor-status-bar')).toBeVisible() + // 状态栏应显示缩放百分比 + await expect(page.locator('.editor-status-bar .status-item')).toContainText(['%']) + }) + + test('可以通过头部 "DCS编辑器" 按钮返回画布列表', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + + // 先进入编辑器 + await page.locator('.add-card').click() + const dialog = page.locator('.el-dialog') + await expect(dialog).toBeVisible({ timeout: 5000 }) + await dialog.locator('input').first().fill('返回测试画布') + await dialog.locator('button').filter({ hasText: '创建' }).click() + await expect(page.locator('.dcs-editor')).toBeVisible({ timeout: 10000 }) + + // 点击 "DCS编辑器" 返回画布列表 + await page.locator('.scope-btn').click() + await expect(page.locator('.canvases-page')).toBeVisible({ timeout: 10000 }) + }) +}) diff --git a/tests/unit/composables/useComponentTemplates.test.ts b/tests/unit/composables/useComponentTemplates.test.ts new file mode 100644 index 0000000..306f051 --- /dev/null +++ b/tests/unit/composables/useComponentTemplates.test.ts @@ -0,0 +1,229 @@ +import type { Layer } from '@/components/editor/canvas/types' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { useComponentTemplates } from '@/composables/useComponentTemplates' + +const RE_TPL_PREFIX = /^tpl_/ +const RE_DEFAULT_NAME = /^模板 \d+$/ + +// 模拟 @vueuse/core 的 useLocalStorage +// useComponentTemplates 通过 useLocalStorage 管理持久化, +// 测试中用 ref 替代以隔离 localStorage 副作用 +vi.mock('@vueuse/core', async () => { + const { ref } = await import('vue') + return { + useLocalStorage: (_key: string, defaultValue: any) => ref(defaultValue), + } +}) + +function createMockLayer(overrides: Partial = {}): Layer { + return { + id: `layer_${Math.random().toString(36).slice(2, 6)}`, + type: 'rect', + x: 0, + y: 0, + width: 100, + height: 50, + ...overrides, + } +} + +describe('useComponentTemplates', () => { + let tpl: ReturnType + + beforeEach(() => { + vi.useFakeTimers() + tpl = useComponentTemplates() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + // ──── saveTemplate() ──── + + describe('saveTemplate() 保存模板', () => { + it('应保存模板并返回模板对象', () => { + const layers = [createMockLayer()] + const result = tpl.saveTemplate('测试模板', layers) + + expect(result).not.toBeNull() + expect(result!.name).toBe('测试模板') + expect(result!.layers).toHaveLength(1) + expect(result!.id).toMatch(RE_TPL_PREFIX) + expect(result!.createdAt).toBeTypeOf('number') + }) + + it('空图层数组时应返回 null', () => { + const result = tpl.saveTemplate('空模板', []) + + expect(result).toBeNull() + }) + + it('名称为空字符串时应使用默认名称', () => { + const layers = [createMockLayer()] + const result = tpl.saveTemplate('', layers) + + expect(result).not.toBeNull() + expect(result!.name).toMatch(RE_DEFAULT_NAME) + }) + + it('名称含前后空格时应自动 trim', () => { + const layers = [createMockLayer()] + const result = tpl.saveTemplate(' 模板名称 ', layers) + + expect(result!.name).toBe('模板名称') + }) + + it('应深拷贝图层数据,修改原图层不影响模板', () => { + const layer = createMockLayer({ x: 10 }) + const layers = [layer] + const result = tpl.saveTemplate('深拷贝测试', layers) + + // 修改原图层 + layer.x = 999 + + expect(result!.layers[0].x).toBe(10) + }) + + it('多次保存应生成不同的 id', () => { + const layers = [createMockLayer()] + + vi.setSystemTime(new Date('2026-01-01T00:00:00Z')) + const t1 = tpl.saveTemplate('模板1', layers) + + vi.setSystemTime(new Date('2026-01-01T00:00:01Z')) + const t2 = tpl.saveTemplate('模板2', layers) + + expect(t1!.id).not.toBe(t2!.id) + }) + }) + + // ──── templates(sortedTemplates)──── + + describe('templates 排序', () => { + it('空状态返回空数组', () => { + expect(tpl.templates.value).toEqual([]) + }) + + it('应按 createdAt 降序排列(新模板在前)', () => { + const layers = [createMockLayer()] + + vi.setSystemTime(new Date('2026-01-01T00:00:00Z')) + tpl.saveTemplate('旧模板', layers) + + vi.setSystemTime(new Date('2026-01-02T00:00:00Z')) + tpl.saveTemplate('新模板', layers) + + expect(tpl.templates.value).toHaveLength(2) + expect(tpl.templates.value[0].name).toBe('新模板') + expect(tpl.templates.value[1].name).toBe('旧模板') + }) + }) + + // ──── getTemplate() ──── + + describe('getTemplate() 按 id 获取模板', () => { + it('存在时返回模板对象', () => { + const layers = [createMockLayer()] + const saved = tpl.saveTemplate('查找测试', layers)! + + const found = tpl.getTemplate(saved.id) + + expect(found).not.toBeNull() + expect(found!.id).toBe(saved.id) + expect(found!.name).toBe('查找测试') + }) + + it('不存在时返回 null', () => { + expect(tpl.getTemplate('nonexistent_id')).toBeNull() + }) + }) + + // ──── removeTemplate() ──── + + describe('removeTemplate() 删除模板', () => { + it('应按 id 删除指定模板', () => { + const layers = [createMockLayer()] + const t1 = tpl.saveTemplate('模板1', layers)! + const t2 = tpl.saveTemplate('模板2', layers)! + + tpl.removeTemplate(t1.id) + + expect(tpl.templates.value).toHaveLength(1) + expect(tpl.templates.value[0].id).toBe(t2.id) + }) + + it('删除不存在的 id 不应报错', () => { + const layers = [createMockLayer()] + tpl.saveTemplate('模板', layers) + + expect(() => tpl.removeTemplate('nonexistent')).not.toThrow() + expect(tpl.templates.value).toHaveLength(1) + }) + }) + + // ──── renameTemplate() ──── + + describe('renameTemplate() 重命名模板', () => { + it('应更新指定模板的名称', () => { + const layers = [createMockLayer()] + const saved = tpl.saveTemplate('原始名称', layers)! + + tpl.renameTemplate(saved.id, '新名称') + + const updated = tpl.getTemplate(saved.id) + expect(updated!.name).toBe('新名称') + }) + + it('名称含空格时应自动 trim', () => { + const layers = [createMockLayer()] + const saved = tpl.saveTemplate('原始', layers)! + + tpl.renameTemplate(saved.id, ' 修改后 ') + + expect(tpl.getTemplate(saved.id)!.name).toBe('修改后') + }) + + it('重命名不存在的 id 不应报错', () => { + expect(() => tpl.renameTemplate('nonexistent', '新名称')).not.toThrow() + }) + + it('重命名不应影响其他字段', () => { + const layers = [createMockLayer({ x: 42 })] + const saved = tpl.saveTemplate('原始', layers)! + const originalCreatedAt = saved.createdAt + + tpl.renameTemplate(saved.id, '新名称') + + const updated = tpl.getTemplate(saved.id)! + expect(updated.createdAt).toBe(originalCreatedAt) + expect(updated.layers[0].x).toBe(42) + }) + }) + + // ──── 边界情况 ──── + + describe('边界情况', () => { + it('保存含多个图层的模板', () => { + const layers = [ + createMockLayer({ x: 0, y: 0 }), + createMockLayer({ x: 100, y: 100 }), + createMockLayer({ x: 200, y: 200 }), + ] + const result = tpl.saveTemplate('多图层模板', layers) + + expect(result!.layers).toHaveLength(3) + }) + + it('图层含嵌套 config 对象时应深拷贝', () => { + const config = { fill: 'red', nested: { value: 1 } } + const layer = createMockLayer({ config }) + const result = tpl.saveTemplate('嵌套配置', [layer]) + + // 修改原始 config + config.nested.value = 999 + + expect(result!.layers[0].config!.nested.value).toBe(1) + }) + }) +}) diff --git a/tests/unit/composables/useOperationLog.test.ts b/tests/unit/composables/useOperationLog.test.ts new file mode 100644 index 0000000..4239be6 --- /dev/null +++ b/tests/unit/composables/useOperationLog.test.ts @@ -0,0 +1,182 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { useOperationLog } from '@/composables/useOperationLog' + +describe('useOperationLog', () => { + let opLog: ReturnType + + beforeEach(() => { + opLog = useOperationLog() + opLog.clear() + opLog.filterType.value = '' + opLog.searchText.value = '' + }) + + // ──── log() ──── + + describe('log() 添加日志条目', () => { + it('应添加一条日志到 entries 列表头部', () => { + opLog.log('创建', '图层A', '新建矩形图层') + + expect(opLog.entries.value).toHaveLength(1) + expect(opLog.entries.value[0]).toMatchObject({ + action: '创建', + target: '图层A', + detail: '新建矩形图层', + }) + expect(opLog.entries.value[0].id).toBeTruthy() + expect(opLog.entries.value[0].timestamp).toBeTruthy() + }) + + it('新日志应排在列表最前面(降序)', () => { + opLog.log('创建', '图层A', '第一条') + opLog.log('删除', '图层B', '第二条') + + expect(opLog.entries.value[0].action).toBe('删除') + expect(opLog.entries.value[1].action).toBe('创建') + }) + + it('每条日志应有唯一 id', () => { + opLog.log('创建', '图层A', '详情A') + opLog.log('创建', '图层B', '详情B') + + const ids = opLog.entries.value.map(e => e.id) + expect(new Set(ids).size).toBe(2) + }) + + it('超过 500 条时应截断旧日志', () => { + for (let i = 0; i < 510; i++) { + opLog.log('操作', `目标${i}`, `详情${i}`) + } + + expect(opLog.entries.value).toHaveLength(500) + // 最新的应该在最前面 + expect(opLog.entries.value[0].detail).toBe('详情509') + }) + }) + + // ──── clear() ──── + + describe('clear() 清空日志', () => { + it('应清空所有条目', () => { + opLog.log('创建', '图层', '详情') + opLog.log('删除', '图层', '详情') + expect(opLog.entries.value.length).toBeGreaterThan(0) + + opLog.clear() + + expect(opLog.entries.value).toHaveLength(0) + }) + + it('清空后 filteredEntries 也应为空', () => { + opLog.log('创建', '图层', '详情') + opLog.clear() + + expect(opLog.filteredEntries.value).toHaveLength(0) + }) + }) + + // ──── filteredEntries ──── + + describe('filteredEntries 过滤逻辑', () => { + beforeEach(() => { + opLog.log('创建', '图层A', '新建矩形') + opLog.log('删除', '图层B', '删除圆形') + opLog.log('修改', '图层C', '修改颜色属性') + opLog.log('创建', '图层D', '新建文本') + }) + + it('无过滤条件时应返回全部条目', () => { + expect(opLog.filteredEntries.value).toHaveLength(4) + }) + + it('按 filterType 过滤 action', () => { + opLog.filterType.value = '创建' + + expect(opLog.filteredEntries.value).toHaveLength(2) + expect(opLog.filteredEntries.value.every(e => e.action === '创建')).toBe(true) + }) + + it('按 searchText 模糊搜索 detail', () => { + opLog.searchText.value = '矩形' + + expect(opLog.filteredEntries.value).toHaveLength(1) + expect(opLog.filteredEntries.value[0].detail).toBe('新建矩形') + }) + + it('按 searchText 模糊搜索 target', () => { + opLog.searchText.value = '图层B' + + expect(opLog.filteredEntries.value).toHaveLength(1) + expect(opLog.filteredEntries.value[0].target).toBe('图层B') + }) + + it('searchText 不区分大小写', () => { + opLog.log('创建', 'LayerX', 'New Rectangle') + opLog.searchText.value = 'rectangle' + + expect(opLog.filteredEntries.value).toHaveLength(1) + expect(opLog.filteredEntries.value[0].detail).toBe('New Rectangle') + }) + + it('同时使用 filterType 和 searchText', () => { + opLog.filterType.value = '创建' + opLog.searchText.value = '文本' + + expect(opLog.filteredEntries.value).toHaveLength(1) + expect(opLog.filteredEntries.value[0].target).toBe('图层D') + }) + + it('过滤条件无匹配时返回空数组', () => { + opLog.filterType.value = '不存在的操作' + + expect(opLog.filteredEntries.value).toHaveLength(0) + }) + }) + + // ──── exportJSON() ──── + + describe('exportJSON() 导出日志', () => { + it('应创建下载链接并触发点击', () => { + opLog.log('创建', '图层', '详情') + + const mockClick = vi.fn() + const mockAppendChild = vi.spyOn(document.body, 'appendChild').mockImplementation(node => node) + const mockRemoveChild = vi.spyOn(document.body, 'removeChild').mockImplementation(node => node) + const mockCreateElement = vi.spyOn(document, 'createElement').mockReturnValue({ + get href() { return '' }, + set href(_: string) {}, + get download() { return '' }, + set download(_: string) {}, + click: mockClick, + } as unknown as HTMLAnchorElement) + const mockRevokeObjectURL = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {}) + const mockCreateObjectURL = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:test') + + opLog.exportJSON() + + expect(mockCreateObjectURL).toHaveBeenCalled() + expect(mockClick).toHaveBeenCalled() + expect(mockRevokeObjectURL).toHaveBeenCalledWith('blob:test') + + mockAppendChild.mockRestore() + mockRemoveChild.mockRestore() + mockCreateElement.mockRestore() + mockRevokeObjectURL.mockRestore() + mockCreateObjectURL.mockRestore() + }) + }) + + // ──── 单例共享 ──── + + describe('单例模式', () => { + it('多次调用 useOperationLog() 应共享同一份状态', () => { + const log1 = useOperationLog() + const log2 = useOperationLog() + + log1.log('测试', '目标', '详情') + + expect(log2.entries.value).toHaveLength(log1.entries.value.length) + expect(log2.entries.value[0].id).toBe(log1.entries.value[0].id) + }) + }) +}) diff --git a/tests/unit/composables/useRuntimeConsole.test.ts b/tests/unit/composables/useRuntimeConsole.test.ts new file mode 100644 index 0000000..e8b3d3f --- /dev/null +++ b/tests/unit/composables/useRuntimeConsole.test.ts @@ -0,0 +1,304 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { useRuntimeConsole } from '@/composables/useRuntimeConsole' + +describe('useRuntimeConsole', () => { + let console_: ReturnType + + beforeEach(() => { + console_ = useRuntimeConsole() + console_.clear() + console_.levelFilter.value = '' + console_.categoryFilter.value = '' + console_.searchText.value = '' + }) + + // ──── log() ──── + + describe('log() 添加运行日志', () => { + it('应添加一条日志到列表头部', () => { + console_.log('INFO', '系统', '启动完成') + + expect(console_.entries.value).toHaveLength(1) + expect(console_.entries.value[0]).toMatchObject({ + level: 'INFO', + category: '系统', + message: '启动完成', + }) + }) + + it('应支持 quality 和 source 可选参数', () => { + console_.log('WARN', '信号', '信号质量下降', 'BAD', 'PLC-01') + + const entry = console_.entries.value[0] + expect(entry.quality).toBe('BAD') + expect(entry.source).toBe('PLC-01') + }) + + it('quality 和 source 未提供时应为 undefined', () => { + console_.log('INFO', '系统', '正常运行') + + const entry = console_.entries.value[0] + expect(entry.quality).toBeUndefined() + expect(entry.source).toBeUndefined() + }) + + it('新日志应排在最前面', () => { + console_.log('INFO', '系统', '第一条') + console_.log('ERROR', '告警', '第二条') + + expect(console_.entries.value[0].message).toBe('第二条') + expect(console_.entries.value[1].message).toBe('第一条') + }) + + it('超过 1000 条时应截断旧日志', () => { + for (let i = 0; i < 1010; i++) { + console_.log('INFO', '系统', `消息${i}`) + } + + expect(console_.entries.value).toHaveLength(1000) + expect(console_.entries.value[0].message).toBe('消息1009') + }) + + it('每条日志应有唯一 id 和时间戳', () => { + console_.log('INFO', '系统', '消息A') + console_.log('INFO', '系统', '消息B') + + const [a, b] = console_.entries.value + expect(a.id).not.toBe(b.id) + expect(a.timestamp).toBeTruthy() + expect(b.timestamp).toBeTruthy() + }) + }) + + // ──── 便捷方法 info/warn/error ──── + + describe('info() 便捷方法', () => { + it('应以 INFO 级别记录', () => { + console_.info('系统', '信息消息') + + expect(console_.entries.value[0].level).toBe('INFO') + expect(console_.entries.value[0].message).toBe('信息消息') + }) + + it('应支持 quality 参数', () => { + console_.info('信号', '信号正常', 'GOOD') + + expect(console_.entries.value[0].quality).toBe('GOOD') + }) + }) + + describe('warn() 便捷方法', () => { + it('应以 WARN 级别记录', () => { + console_.warn('系统', '警告消息') + + expect(console_.entries.value[0].level).toBe('WARN') + }) + + it('应支持 quality 参数', () => { + console_.warn('信号', '信号不确定', 'UNCERTAIN') + + expect(console_.entries.value[0].quality).toBe('UNCERTAIN') + }) + }) + + describe('error() 便捷方法', () => { + it('应以 ERROR 级别记录', () => { + console_.error('系统', '错误消息') + + expect(console_.entries.value[0].level).toBe('ERROR') + }) + + it('应支持 source 参数', () => { + console_.error('通信', '连接超时', 'PLC-02') + + expect(console_.entries.value[0].source).toBe('PLC-02') + }) + + it('quality 应为 undefined(error 不传 quality)', () => { + console_.error('系统', '错误消息', 'source') + + expect(console_.entries.value[0].quality).toBeUndefined() + }) + }) + + // ──── clear() ──── + + describe('clear() 清空日志', () => { + it('应清空所有条目', () => { + console_.info('系统', '消息1') + console_.warn('告警', '消息2') + console_.clear() + + expect(console_.entries.value).toHaveLength(0) + }) + }) + + // ──── filteredEntries ──── + + describe('filteredEntries 过滤逻辑', () => { + beforeEach(() => { + console_.info('系统', '系统启动') + console_.warn('信号', '信号波动') + console_.error('通信', '连接中断', 'PLC-01') + console_.info('信号', '信号恢复正常') + }) + + it('无过滤条件时应返回全部', () => { + expect(console_.filteredEntries.value).toHaveLength(4) + }) + + it('按 levelFilter 过滤级别', () => { + console_.levelFilter.value = 'INFO' + + expect(console_.filteredEntries.value).toHaveLength(2) + expect(console_.filteredEntries.value.every(e => e.level === 'INFO')).toBe(true) + }) + + it('按 categoryFilter 过滤分类', () => { + console_.categoryFilter.value = '信号' + + expect(console_.filteredEntries.value).toHaveLength(2) + expect(console_.filteredEntries.value.every(e => e.category === '信号')).toBe(true) + }) + + it('按 searchText 模糊搜索 message', () => { + console_.searchText.value = '连接' + + expect(console_.filteredEntries.value).toHaveLength(1) + expect(console_.filteredEntries.value[0].message).toBe('连接中断') + }) + + it('按 searchText 模糊搜索 source', () => { + console_.searchText.value = 'PLC' + + expect(console_.filteredEntries.value).toHaveLength(1) + expect(console_.filteredEntries.value[0].source).toBe('PLC-01') + }) + + it('按 searchText 模糊搜索 category', () => { + console_.searchText.value = '通信' + + expect(console_.filteredEntries.value).toHaveLength(1) + expect(console_.filteredEntries.value[0].category).toBe('通信') + }) + + it('searchText 不区分大小写', () => { + console_.log('INFO', 'System', 'Connection OK', undefined, 'Device-A') + console_.searchText.value = 'connection' + + expect(console_.filteredEntries.value).toHaveLength(1) + expect(console_.filteredEntries.value[0].message).toBe('Connection OK') + }) + + it('多个过滤条件组合使用', () => { + console_.levelFilter.value = 'INFO' + console_.categoryFilter.value = '信号' + + expect(console_.filteredEntries.value).toHaveLength(1) + expect(console_.filteredEntries.value[0].message).toBe('信号恢复正常') + }) + + it('三个过滤条件同时使用', () => { + console_.levelFilter.value = 'INFO' + console_.categoryFilter.value = '信号' + console_.searchText.value = '恢复' + + expect(console_.filteredEntries.value).toHaveLength(1) + expect(console_.filteredEntries.value[0].message).toBe('信号恢复正常') + }) + + it('过滤条件无匹配时返回空数组', () => { + console_.levelFilter.value = 'ERROR' + console_.categoryFilter.value = '系统' + + expect(console_.filteredEntries.value).toHaveLength(0) + }) + }) + + // ──── categories ──── + + describe('categories 计算属性', () => { + it('空日志时返回空数组', () => { + expect(console_.categories.value).toEqual([]) + }) + + it('应返回去重排序后的分类列表', () => { + console_.info('系统', '消息1') + console_.info('信号', '消息2') + console_.warn('系统', '消息3') + console_.error('通信', '消息4') + + expect(console_.categories.value).toEqual(['信号', '系统', '通信']) + }) + }) + + // ──── levelCounts ──── + + describe('levelCounts 计算属性', () => { + it('空日志时所有计数为零', () => { + expect(console_.levelCounts.value).toEqual({ INFO: 0, WARN: 0, ERROR: 0 }) + }) + + it('应正确统计各级别数量', () => { + console_.info('系统', '消息1') + console_.info('系统', '消息2') + console_.warn('告警', '消息3') + console_.error('通信', '消息4') + console_.error('通信', '消息5') + console_.error('通信', '消息6') + + expect(console_.levelCounts.value).toEqual({ + INFO: 2, + WARN: 1, + ERROR: 3, + }) + }) + }) + + // ──── exportJSON() ──── + + describe('exportJSON() 导出日志', () => { + it('应创建 Blob 下载并清理', () => { + console_.info('系统', '测试消息') + + const mockClick = vi.fn() + const mockAppendChild = vi.spyOn(document.body, 'appendChild').mockImplementation(node => node) + const mockRemoveChild = vi.spyOn(document.body, 'removeChild').mockImplementation(node => node) + const mockCreateElement = vi.spyOn(document, 'createElement').mockReturnValue({ + get href() { return '' }, + set href(_: string) {}, + get download() { return '' }, + set download(_: string) {}, + click: mockClick, + } as unknown as HTMLAnchorElement) + const mockRevokeObjectURL = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {}) + const mockCreateObjectURL = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:test') + + console_.exportJSON() + + expect(mockCreateObjectURL).toHaveBeenCalled() + expect(mockClick).toHaveBeenCalled() + expect(mockRevokeObjectURL).toHaveBeenCalledWith('blob:test') + + mockAppendChild.mockRestore() + mockRemoveChild.mockRestore() + mockCreateElement.mockRestore() + mockRevokeObjectURL.mockRestore() + mockCreateObjectURL.mockRestore() + }) + }) + + // ──── 单例共享 ──── + + describe('单例模式', () => { + it('多次调用 useRuntimeConsole() 应共享同一份状态', () => { + const c1 = useRuntimeConsole() + const c2 = useRuntimeConsole() + + c1.info('系统', '测试') + + expect(c2.entries.value).toHaveLength(c1.entries.value.length) + expect(c2.entries.value[0].id).toBe(c1.entries.value[0].id) + }) + }) +}) diff --git a/tests/unit/runtime/expressionEval.test.ts b/tests/unit/runtime/expressionEval.test.ts new file mode 100644 index 0000000..8cfc4b1 --- /dev/null +++ b/tests/unit/runtime/expressionEval.test.ts @@ -0,0 +1,713 @@ +import type { CanvasRuntimeVariable } from '@/components/editor/canvas/context/runtime' +import type { Layer, LayerBindingDefinition } from '@/components/editor/canvas/types' +import { describe, expect, it, vi } from 'vitest' +import { + resolveLayerAppearance, + resolveLayerBindingValue, + resolveLayerDisplayValue, +} from '@/components/editor/components/runtime' + +// ──── 工具函数:构造测试用 Layer ──── + +function createLayer(overrides: Partial = {}): Layer { + return { + id: 'layer-1', + type: 'text', + x: 0, + y: 0, + width: 200, + height: 40, + ...overrides, + } +} + +function createVariableMap(entries: Record): Record { + const map: Record = {} + for (const [path, { value, moduleLabel, propLabel }] of Object.entries(entries)) { + map[path] = { + path, + moduleLabel: moduleLabel ?? path.split('.')[0] ?? 'MOD', + moduleName: 'Module', + propLabel: propLabel ?? path.split('.')[1] ?? path, + propName: 'Property', + type: 'analog', + value, + } + } + return map +} + +// ============================================ +// resolveLayerDisplayValue +// ============================================ + +describe('resolveLayerDisplayValue — 图层显示值解析', () => { + const emptyVarMap: Record = {} + + describe('text 类型图层', () => { + it('无绑定时返回 config.content 的值', () => { + const layer = createLayer({ + type: 'text', + config: { content: '静态文本' }, + }) + expect(resolveLayerDisplayValue(layer, emptyVarMap)).toBe('静态文本') + }) + + it('无绑定且无 config 时返回空字符串', () => { + const layer = createLayer({ type: 'text' }) + expect(resolveLayerDisplayValue(layer, emptyVarMap)).toBe('') + }) + + it('有绑定时优先使用绑定值', () => { + const layer = createLayer({ + type: 'text', + config: { content: '静态文本' }, + bindings: { value: 'MOD.temperature' }, + }) + const varMap = createVariableMap({ + 'MOD.temperature': { value: '高温告警' }, + }) + expect(resolveLayerDisplayValue(layer, varMap)).toBe('高温告警') + }) + }) + + describe('number 类型图层', () => { + it('无绑定时显示默认数值 0 并保留 2 位小数', () => { + const layer = createLayer({ type: 'number' }) + expect(resolveLayerDisplayValue(layer, emptyVarMap)).toBe('0.00') + }) + + it('有绑定时显示变量值并格式化', () => { + const layer = createLayer({ + type: 'number', + config: { decimals: 1 }, + bindings: { value: 'MOD.temperature' }, + }) + const varMap = createVariableMap({ + 'MOD.temperature': { value: 36.567 }, + }) + expect(resolveLayerDisplayValue(layer, varMap)).toBe('36.6') + }) + + it('应正确应用前缀和后缀', () => { + const layer = createLayer({ + type: 'number', + config: { decimals: 0, prefix: '温度:', suffix: '℃' }, + bindings: { value: 'MOD.temp' }, + }) + const varMap = createVariableMap({ 'MOD.temp': { value: 25 } }) + expect(resolveLayerDisplayValue(layer, varMap)).toBe('温度:25℃') + }) + + it('非数值绑定应回退为 0', () => { + const layer = createLayer({ + type: 'number', + config: { decimals: 2 }, + bindings: { value: 'MOD.invalid' }, + }) + const varMap = createVariableMap({ 'MOD.invalid': { value: 'not-a-number' } }) + expect(resolveLayerDisplayValue(layer, varMap)).toBe('0.00') + }) + + it('小数位 decimals 配置为 0 时不显示小数部分', () => { + const layer = createLayer({ + type: 'number', + config: { decimals: 0 }, + bindings: { value: 'MOD.count' }, + }) + const varMap = createVariableMap({ 'MOD.count': { value: 42.999 } }) + expect(resolveLayerDisplayValue(layer, varMap)).toBe('43') + }) + }) + + describe('rect 类型图层', () => { + it('无绑定且无 value 时返回空字符串', () => { + const layer = createLayer({ type: 'rect' }) + expect(resolveLayerDisplayValue(layer, emptyVarMap)).toBe('') + }) + }) + + describe('null / undefined 值处理', () => { + it('绑定值为 null 时返回空字符串', () => { + const layer = createLayer({ + type: 'text', + bindings: { value: 'MOD.nullVar' }, + }) + const varMap = createVariableMap({ 'MOD.nullVar': { value: null } }) + // 变量存在但值为 null → 在 resolveLayerBindingValue 中返回 null + // resolveLayerDisplayValue 中 null 走 String 转换前会被拦截返回 '' + expect(resolveLayerDisplayValue(layer, varMap)).toBe('') + }) + }) +}) + +// ============================================ +// resolveLayerBindingValue +// ============================================ + +describe('resolveLayerBindingValue — 绑定值解析', () => { + describe('variable 类型绑定', () => { + it('字符串绑定应解析为变量引用', () => { + const layer = createLayer({ + bindings: { value: 'MOD.pressure' }, + }) + const varMap = createVariableMap({ 'MOD.pressure': { value: 101.3 } }) + expect(resolveLayerBindingValue(layer, 'value', varMap)).toBe(101.3) + }) + + it('变量不存在时应返回 undefined', () => { + const layer = createLayer({ + bindings: { value: 'MOD.nonexistent' }, + }) + expect(resolveLayerBindingValue(layer, 'value', {})).toBeUndefined() + }) + + it('无绑定定义时应返回 undefined', () => { + const layer = createLayer() + expect(resolveLayerBindingValue(layer, 'value', {})).toBeUndefined() + }) + + it('对象格式的 variable 绑定应正确解析', () => { + const binding: LayerBindingDefinition = { + id: 'b1', + type: 'variable', + value: 'MOD.level', + priority: 0, + } + const layer = createLayer({ + bindings: { value: binding }, + }) + const varMap = createVariableMap({ 'MOD.level': { value: 85 } }) + expect(resolveLayerBindingValue(layer, 'value', varMap)).toBe(85) + }) + }) + + describe('expression 类型绑定', () => { + it('简单数学表达式应正确计算', () => { + const layer = createLayer({ + bindings: { + value: { + id: 'e1', + type: 'expression', + value: 'vars["MOD.a"] + vars["MOD.b"]', + variables: ['MOD.a', 'MOD.b'], + priority: 0, + }, + }, + }) + const varMap = createVariableMap({ + 'MOD.a': { value: 10 }, + 'MOD.b': { value: 20 }, + }) + expect(resolveLayerBindingValue(layer, 'value', varMap)).toBe(30) + }) + + it('三元表达式应正确计算', () => { + const layer = createLayer({ + bindings: { + value: { + id: 'e2', + type: 'expression', + value: 'vars["MOD.flag"] ? "开启" : "关闭"', + variables: ['MOD.flag'], + priority: 0, + }, + }, + }) + const varMap = createVariableMap({ 'MOD.flag': { value: true } }) + expect(resolveLayerBindingValue(layer, 'value', varMap)).toBe('开启') + }) + + it('使用 Math 内置对象的表达式', () => { + const layer = createLayer({ + bindings: { + value: { + id: 'e3', + type: 'expression', + value: 'Math.round(vars["MOD.val"] * 100) / 100', + variables: ['MOD.val'], + priority: 0, + }, + }, + }) + const varMap = createVariableMap({ 'MOD.val': { value: 3.14159 } }) + expect(resolveLayerBindingValue(layer, 'value', varMap)).toBe(3.14) + }) + + it('无效表达式应返回 undefined 且不抛异常', () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + const layer = createLayer({ + bindings: { + value: { + id: 'e4', + type: 'expression', + value: '!!!invalid syntax{{{', + priority: 0, + }, + }, + }) + expect(resolveLayerBindingValue(layer, 'value', {})).toBeUndefined() + expect(consoleSpy).toHaveBeenCalled() + consoleSpy.mockRestore() + }) + + it('通过模块标签访问变量的表达式', () => { + const layer = createLayer({ + bindings: { + value: { + id: 'e5', + type: 'expression', + value: 'MOD["temperature"] * 1.8 + 32', + variables: ['MOD.temperature'], + priority: 0, + }, + }, + }) + const varMap = createVariableMap({ + 'MOD.temperature': { value: 100, moduleLabel: 'MOD', propLabel: 'temperature' }, + }) + // 100 * 1.8 + 32 = 212 (摄氏转华氏) + expect(resolveLayerBindingValue(layer, 'value', varMap)).toBe(212) + }) + }) + + describe('绑定优先级选择', () => { + it('多条绑定规则取优先级最高的', () => { + const layer = createLayer({ + bindings: { + value: [ + { + id: 'low', + type: 'variable', + value: 'MOD.low', + priority: 1, + }, + { + id: 'high', + type: 'variable', + value: 'MOD.high', + priority: 10, + }, + { + id: 'mid', + type: 'variable', + value: 'MOD.mid', + priority: 5, + }, + ], + }, + }) + const varMap = createVariableMap({ + 'MOD.low': { value: 'low' }, + 'MOD.high': { value: 'high' }, + 'MOD.mid': { value: 'mid' }, + }) + expect(resolveLayerBindingValue(layer, 'value', varMap)).toBe('high') + }) + + it('优先级相同时取最后出现的', () => { + const layer = createLayer({ + bindings: { + value: [ + { + id: 'a', + type: 'variable', + value: 'MOD.first', + priority: 0, + }, + { + id: 'b', + type: 'variable', + value: 'MOD.second', + priority: 0, + }, + ], + }, + }) + const varMap = createVariableMap({ + 'MOD.first': { value: 'first' }, + 'MOD.second': { value: 'second' }, + }) + // 同优先级时,后者 priority 不大于前者,所以前者保持选中 + // 源码: priority > selectedPriority → 仅严格大于才替换,所以 first 胜出 + expect(resolveLayerBindingValue(layer, 'value', varMap)).toBe('first') + }) + }) + + describe('enabled/disabled 绑定过滤', () => { + it('enabled: false 的绑定应被跳过', () => { + const layer = createLayer({ + bindings: { + value: [ + { + id: 'disabled', + type: 'variable', + value: 'MOD.disabled_val', + priority: 100, + enabled: false, + }, + { + id: 'enabled', + type: 'variable', + value: 'MOD.enabled_val', + priority: 1, + }, + ], + }, + }) + const varMap = createVariableMap({ + 'MOD.disabled_val': { value: '不应出现' }, + 'MOD.enabled_val': { value: '应该出现' }, + }) + expect(resolveLayerBindingValue(layer, 'value', varMap)).toBe('应该出现') + }) + + it('所有绑定都 disabled 时应返回 undefined', () => { + const layer = createLayer({ + bindings: { + value: [ + { + id: 'd1', + type: 'variable', + value: 'MOD.a', + priority: 1, + enabled: false, + }, + { + id: 'd2', + type: 'variable', + value: 'MOD.b', + priority: 2, + enabled: false, + }, + ], + }, + }) + expect(resolveLayerBindingValue(layer, 'value', {})).toBeUndefined() + }) + + it('绑定值为空字符串时应被跳过', () => { + const layer = createLayer({ + bindings: { + value: [ + { + id: 'empty', + type: 'variable', + value: '', + priority: 100, + }, + { + id: 'valid', + type: 'variable', + value: 'MOD.val', + priority: 1, + }, + ], + }, + }) + const varMap = createVariableMap({ 'MOD.val': { value: 42 } }) + expect(resolveLayerBindingValue(layer, 'value', varMap)).toBe(42) + }) + + it('绑定值为纯空格时应被跳过', () => { + const layer = createLayer({ + bindings: { + value: { + id: 'spaces', + type: 'variable', + value: ' ', + priority: 0, + }, + }, + }) + expect(resolveLayerBindingValue(layer, 'value', {})).toBeUndefined() + }) + }) +}) + +// ============================================ +// resolveLayerAppearance +// ============================================ + +describe('resolveLayerAppearance — 外观样式解析', () => { + const emptyVarMap: Record = {} + const autoTextColor = '#111827' + + describe('文字颜色解析', () => { + it('无 style 和 config 时使用 autoTextColor', () => { + const layer = createLayer({ type: 'text' }) + const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap) + expect(result.textColor).toBe(autoTextColor) + }) + + it('style.text.color 优先于 autoTextColor', () => { + const layer = createLayer({ + type: 'text', + style: { text: { color: '#FF0000' } }, + }) + const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap) + expect(result.textColor).toBe('#FF0000') + }) + + it('config.textColor 作为备选', () => { + const layer = createLayer({ + type: 'text', + config: { textColor: '#00FF00' }, + }) + const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap) + expect(result.textColor).toBe('#00FF00') + }) + }) + + describe('背景颜色解析', () => { + it('rect 类型无配置时使用默认填充色 #D7EBFF', () => { + const layer = createLayer({ type: 'rect' }) + const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap) + expect(result.backgroundColor).not.toBeNull() + expect(result.backgroundColor!.startsWith('#D7EBFF')).toBe(true) + }) + + it('text 类型无配置时背景为 null', () => { + const layer = createLayer({ type: 'text' }) + const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap) + expect(result.backgroundColor).toBeNull() + }) + + it('通过 style.fill.color 设置背景色', () => { + const layer = createLayer({ + type: 'text', + style: { fill: { color: '#AABBCC' } }, + }) + const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap) + expect(result.backgroundColor).not.toBeNull() + expect(result.backgroundColor!.startsWith('#AABBCC')).toBe(true) + }) + }) + + describe('边框样式解析', () => { + it('无配置时边框宽度为 0', () => { + const layer = createLayer({ type: 'rect' }) + const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap) + expect(result.borderWidth).toBe(0) + expect(result.borderStyle).toBe('solid') + }) + + it('应正确读取 style.border 属性', () => { + const layer = createLayer({ + type: 'rect', + style: { + border: { color: '#333', width: 2, style: 'dashed', radius: 8 }, + }, + }) + const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap) + expect(result.borderColor).toBe('#333') + expect(result.borderWidth).toBe(2) + expect(result.borderStyle).toBe('dashed') + expect(result.borderRadius).toBe(8) + }) + + it('负数边框宽度应被 clamp 为 0', () => { + const layer = createLayer({ + type: 'rect', + style: { border: { width: -5 } }, + }) + const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap) + expect(result.borderWidth).toBe(0) + }) + }) + + describe('字体样式解析', () => { + it('text 类型默认字号为 18', () => { + const layer = createLayer({ type: 'text' }) + const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap) + expect(result.fontSize).toBe(18) + }) + + it('number 类型默认字号为 24', () => { + const layer = createLayer({ type: 'number' }) + const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap) + expect(result.fontSize).toBe(24) + }) + + it('字号最小值为 10', () => { + const layer = createLayer({ + type: 'text', + style: { text: { fontSize: 5 } }, + }) + const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap) + expect(result.fontSize).toBe(10) + }) + + it('number 类型默认字重为 700', () => { + const layer = createLayer({ type: 'number' }) + const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap) + expect(result.fontWeight).toBe(700) + }) + + it('text 类型默认字重为 500', () => { + const layer = createLayer({ type: 'text' }) + const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap) + expect(result.fontWeight).toBe(500) + }) + + it('默认对齐方式为 center / middle', () => { + const layer = createLayer({ type: 'text' }) + const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap) + expect(result.textAlign).toBe('center') + expect(result.verticalAlign).toBe('middle') + }) + }) + + describe('阴影样式解析', () => { + it('无阴影配置时 boxShadow 为 none', () => { + const layer = createLayer({ type: 'rect' }) + const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap) + expect(result.boxShadow).toBe('none') + }) + + it('有阴影配置时应生成 CSS box-shadow 值', () => { + const layer = createLayer({ + type: 'rect', + style: { + shadow: { + color: '#000000', + blur: 10, + offsetX: 2, + offsetY: 4, + enabled: true, + }, + }, + }) + const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap) + expect(result.boxShadow).toBe('2px 4px 10px #000000') + }) + + it('阴影 enabled 为 false 时返回 none', () => { + const layer = createLayer({ + type: 'rect', + style: { + shadow: { + color: '#000000', + blur: 10, + offsetX: 2, + offsetY: 4, + enabled: false, + }, + }, + }) + const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap) + expect(result.boxShadow).toBe('none') + }) + }) + + describe('通过绑定覆盖外观属性', () => { + it('绑定值应覆盖静态样式属性', () => { + const layer = createLayer({ + type: 'text', + style: { text: { color: '#111111' } }, + bindings: { + 'style.text.color': 'MOD.textColor', + }, + }) + const varMap = createVariableMap({ + 'MOD.textColor': { value: '#FF5500' }, + }) + const result = resolveLayerAppearance(layer, autoTextColor, varMap) + expect(result.textColor).toBe('#FF5500') + }) + }) + + describe('displayValue 包含在返回结果中', () => { + it('resolveLayerAppearance 结果包含 displayValue', () => { + const layer = createLayer({ + type: 'text', + config: { content: '测试内容' }, + }) + const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap) + expect(result.displayValue).toBe('测试内容') + }) + }) +}) + +// ============================================ +// 表达式安全沙盒 +// ============================================ + +describe('表达式安全沙盒', () => { + it('应拒绝包含 constructor 的表达式', () => { + const layer = createLayer({ + bindings: { + value: { + id: 'attack1', + type: 'expression', + value: 'this.constructor.constructor("return globalThis")()', + priority: 0, + }, + }, + }) + expect(resolveLayerBindingValue(layer, 'value', {})).toBeUndefined() + }) + + it('应拒绝包含 window 的表达式', () => { + const layer = createLayer({ + bindings: { + value: { + id: 'attack2', + type: 'expression', + value: 'window.location.href', + priority: 0, + }, + }, + }) + expect(resolveLayerBindingValue(layer, 'value', {})).toBeUndefined() + }) + + it('应拒绝包含 __proto__ 的表达式', () => { + const layer = createLayer({ + bindings: { + value: { + id: 'attack3', + type: 'expression', + value: '({}).__proto__', + priority: 0, + }, + }, + }) + expect(resolveLayerBindingValue(layer, 'value', {})).toBeUndefined() + }) + + it('正常表达式应不受影响', () => { + const layer = createLayer({ + bindings: { + value: { + id: 'safe', + type: 'expression', + value: 'vars["MOD.a"] + 10', + variables: ['MOD.a'], + priority: 0, + }, + }, + }) + const varMap = createVariableMap({ 'MOD.a': { value: 5 } }) + expect(resolveLayerBindingValue(layer, 'value', varMap)).toBe(15) + }) + + it('使用 Math 的表达式应正常工作', () => { + const layer = createLayer({ + bindings: { + value: { + id: 'math', + type: 'expression', + value: 'Math.max(vars["MOD.a"], vars["MOD.b"])', + variables: ['MOD.a', 'MOD.b'], + priority: 0, + }, + }, + }) + const varMap = createVariableMap({ + 'MOD.a': { value: 3 }, + 'MOD.b': { value: 7 }, + }) + expect(resolveLayerBindingValue(layer, 'value', varMap)).toBe(7) + }) +}) diff --git a/tests/unit/schema/componentSchema.test.ts b/tests/unit/schema/componentSchema.test.ts new file mode 100644 index 0000000..6ff3a0e --- /dev/null +++ b/tests/unit/schema/componentSchema.test.ts @@ -0,0 +1,477 @@ +import { + barComponentSchema, + buttonComponentConfigSchema, + buttonComponentSchema, + componentBaseSchema, + componentEventSchema, + componentTypeSchema, + numberComponentConfigSchema, + numberComponentSchema, + rectComponentSchema, + textComponentConfigSchema, + textComponentSchema, +} from '@cslab-dcs/schema/canvas/component' +import { describe, expect, it } from 'vitest' + +// ──── 测试辅助函数 ──── + +function createValidBase(overrides: Record = {}) { + return { + id: '550e8400-e29b-41d4-a716-446655440000', + type: 'rect', + name: '测试组件', + position: { x: 100, y: 200 }, + size: { width: 300, height: 150 }, + ...overrides, + } +} + +// ============================================ +// componentTypeSchema — 组件类型枚举 +// ============================================ + +describe('componentTypeSchema — 组件类型枚举', () => { + it('应接受所有合法的组件类型', () => { + const validTypes = ['rect', 'number', 'text', 'bar', 'button', 'pidController', 'canvasSwitcher', 'custom'] + for (const type of validTypes) { + expect(componentTypeSchema.safeParse(type).success).toBe(true) + } + }) + + it('应拒绝无效的组件类型', () => { + expect(componentTypeSchema.safeParse('invalid').success).toBe(false) + expect(componentTypeSchema.safeParse('').success).toBe(false) + expect(componentTypeSchema.safeParse(123).success).toBe(false) + }) +}) + +// ============================================ +// componentBaseSchema — 组件基础属性 +// ============================================ + +describe('componentBaseSchema — 组件基础属性', () => { + it('最小合法数据应通过验证', () => { + const result = componentBaseSchema.safeParse(createValidBase()) + expect(result.success).toBe(true) + }) + + it('id 必须是合法的 UUID', () => { + const result = componentBaseSchema.safeParse(createValidBase({ id: 'not-a-uuid' })) + expect(result.success).toBe(false) + }) + + it('id 不能为空', () => { + const result = componentBaseSchema.safeParse(createValidBase({ id: '' })) + expect(result.success).toBe(false) + }) + + it('name 不能为空字符串', () => { + const result = componentBaseSchema.safeParse(createValidBase({ name: '' })) + expect(result.success).toBe(false) + }) + + it('position 必须包含 x 和 y', () => { + const result = componentBaseSchema.safeParse(createValidBase({ position: { x: 10 } })) + expect(result.success).toBe(false) + }) + + it('size 的 width 和 height 必须为正数', () => { + const negativeWidth = componentBaseSchema.safeParse(createValidBase({ size: { width: -10, height: 100 } })) + expect(negativeWidth.success).toBe(false) + + const zeroHeight = componentBaseSchema.safeParse(createValidBase({ size: { width: 100, height: 0 } })) + expect(zeroHeight.success).toBe(false) + }) + + it('默认值:zIndex 应为 0', () => { + const result = componentBaseSchema.parse(createValidBase()) + expect(result.zIndex).toBe(0) + }) + + it('默认值:opacity 应为 1', () => { + const result = componentBaseSchema.parse(createValidBase()) + expect(result.opacity).toBe(1) + }) + + it('默认值:visible 应为 true', () => { + const result = componentBaseSchema.parse(createValidBase()) + expect(result.visible).toBe(true) + }) + + it('opacity 必须在 0-1 范围内', () => { + const tooHigh = componentBaseSchema.safeParse(createValidBase({ opacity: 1.5 })) + expect(tooHigh.success).toBe(false) + + const tooLow = componentBaseSchema.safeParse(createValidBase({ opacity: -0.1 })) + expect(tooLow.success).toBe(false) + + const valid = componentBaseSchema.safeParse(createValidBase({ opacity: 0.5 })) + expect(valid.success).toBe(true) + }) + + it('zIndex 必须是整数', () => { + const floatZIndex = componentBaseSchema.safeParse(createValidBase({ zIndex: 1.5 })) + expect(floatZIndex.success).toBe(false) + }) + + it('可选属性 bindings / style / events / metadata 为空时通过验证', () => { + const data = createValidBase() + const result = componentBaseSchema.safeParse(data) + expect(result.success).toBe(true) + }) + + it('events 数组应验证每个事件的格式', () => { + const data = createValidBase({ + events: [ + { + id: 'evt-1', + trigger: 'click', + action: { type: 'callMethod' }, + }, + ], + }) + const result = componentBaseSchema.safeParse(data) + expect(result.success).toBe(true) + }) +}) + +// ============================================ +// componentEventSchema — 事件 Schema +// ============================================ + +describe('componentEventSchema — 事件 Schema', () => { + it('合法事件应通过验证', () => { + const event = { + id: 'evt-1', + trigger: 'click', + action: { type: 'callMethod' }, + } + expect(componentEventSchema.safeParse(event).success).toBe(true) + }) + + it('trigger 只能是 click / dblclick', () => { + const invalid = { + id: 'evt-1', + trigger: 'hover', + action: { type: 'callMethod' }, + } + expect(componentEventSchema.safeParse(invalid).success).toBe(false) + }) + + it('dblclick 触发器应通过验证', () => { + const event = { + id: 'evt-1', + trigger: 'dblclick', + action: { type: 'callMethod' }, + } + expect(componentEventSchema.safeParse(event).success).toBe(true) + }) + + it('action.type 不能为空字符串', () => { + const invalid = { + id: 'evt-1', + trigger: 'click', + action: { type: '' }, + } + expect(componentEventSchema.safeParse(invalid).success).toBe(false) + }) + + it('可选属性 condition 应通过验证', () => { + const event = { + id: 'evt-1', + trigger: 'click', + condition: 'vars.status === "running"', + action: { type: 'navigate', payload: { target: 'page2' } }, + } + const result = componentEventSchema.safeParse(event) + expect(result.success).toBe(true) + }) +}) + +// ============================================ +// rectComponentSchema — 矩形组件 +// ============================================ + +describe('rectComponentSchema — 矩形组件', () => { + it('合法矩形组件应通过验证', () => { + const data = createValidBase({ type: 'rect' }) + expect(rectComponentSchema.safeParse(data).success).toBe(true) + }) + + it('type 必须是 rect', () => { + const data = createValidBase({ type: 'text' }) + expect(rectComponentSchema.safeParse(data).success).toBe(false) + }) + + it('可选 config.fillColor', () => { + const data = createValidBase({ + type: 'rect', + config: { fillColor: '#FF0000' }, + }) + const result = rectComponentSchema.safeParse(data) + expect(result.success).toBe(true) + if (result.success) { + expect(result.data.config?.fillColor).toBe('#FF0000') + } + }) + + it('无 config 时也应通过', () => { + const data = createValidBase({ type: 'rect' }) + expect(rectComponentSchema.safeParse(data).success).toBe(true) + }) +}) + +// ============================================ +// numberComponentSchema / numberComponentConfigSchema — 数字组件 +// ============================================ + +describe('numberComponentConfigSchema — 数字组件配置', () => { + it('空对象应通过(全部可选或有默认值)', () => { + const result = numberComponentConfigSchema.safeParse({}) + expect(result.success).toBe(true) + }) + + it('默认值:decimals 应为 2', () => { + const result = numberComponentConfigSchema.parse({}) + expect(result.decimals).toBe(2) + }) + + it('默认值:thousandsSeparator 应为 false', () => { + const result = numberComponentConfigSchema.parse({}) + expect(result.thousandsSeparator).toBe(false) + }) + + it('默认值:showTrend 应为 false', () => { + const result = numberComponentConfigSchema.parse({}) + expect(result.showTrend).toBe(false) + }) + + it('decimals 必须在 0-10 范围内', () => { + expect(numberComponentConfigSchema.safeParse({ decimals: -1 }).success).toBe(false) + expect(numberComponentConfigSchema.safeParse({ decimals: 11 }).success).toBe(false) + expect(numberComponentConfigSchema.safeParse({ decimals: 5 }).success).toBe(true) + }) + + it('decimals 必须是整数', () => { + expect(numberComponentConfigSchema.safeParse({ decimals: 1.5 }).success).toBe(false) + }) + + it('prefix 和 suffix 应为字符串', () => { + const result = numberComponentConfigSchema.safeParse({ + prefix: 'T=', + suffix: '℃', + }) + expect(result.success).toBe(true) + }) +}) + +describe('numberComponentSchema — 数字组件', () => { + it('合法数字组件应通过验证', () => { + const data = createValidBase({ + type: 'number', + config: { decimals: 3, suffix: '℃' }, + }) + expect(numberComponentSchema.safeParse(data).success).toBe(true) + }) + + it('type 必须是 number', () => { + const data = createValidBase({ type: 'rect' }) + expect(numberComponentSchema.safeParse(data).success).toBe(false) + }) +}) + +// ============================================ +// textComponentSchema / textComponentConfigSchema — 文本组件 +// ============================================ + +describe('textComponentConfigSchema — 文本组件配置', () => { + it('空对象应通过', () => { + expect(textComponentConfigSchema.safeParse({}).success).toBe(true) + }) + + it('默认值:content 应为空字符串', () => { + const result = textComponentConfigSchema.parse({}) + expect(result.content).toBe('') + }) + + it('默认值:isDynamic 应为 false', () => { + const result = textComponentConfigSchema.parse({}) + expect(result.isDynamic).toBe(false) + }) + + it('包含所有字段时应通过验证', () => { + const config = { + fillColor: '#FFFFFF', + textColor: '#000000', + content: '测试内容', + isDynamic: true, + expression: 'vars["MOD.val"]', + } + const result = textComponentConfigSchema.safeParse(config) + expect(result.success).toBe(true) + }) +}) + +describe('textComponentSchema — 文本组件', () => { + it('合法文本组件应通过验证', () => { + const data = createValidBase({ + type: 'text', + config: { content: 'Hello' }, + }) + expect(textComponentSchema.safeParse(data).success).toBe(true) + }) +}) + +// ============================================ +// barComponentSchema — 棒图组件 +// ============================================ + +describe('barComponentSchema — 棒图组件', () => { + it('合法棒图组件应通过验证', () => { + const data = createValidBase({ + type: 'bar', + config: { min: 0, max: 100, direction: 'vertical' }, + }) + expect(barComponentSchema.safeParse(data).success).toBe(true) + }) + + it('config 可以包含分段颜色', () => { + const data = createValidBase({ + type: 'bar', + config: { + min: 0, + max: 100, + colors: [ + { threshold: 30, color: '#00FF00' }, + { threshold: 70, color: '#FFFF00' }, + { threshold: 100, color: '#FF0000' }, + ], + }, + }) + const result = barComponentSchema.safeParse(data) + expect(result.success).toBe(true) + }) + + it('direction 只能是 horizontal 或 vertical', () => { + const data = createValidBase({ + type: 'bar', + config: { direction: 'diagonal' }, + }) + const result = barComponentSchema.safeParse(data) + expect(result.success).toBe(false) + }) +}) + +// ============================================ +// buttonComponentSchema / buttonComponentConfigSchema — 按钮组件 +// ============================================ + +describe('buttonComponentConfigSchema — 按钮组件配置', () => { + it('空对象应通过(使用默认值)', () => { + expect(buttonComponentConfigSchema.safeParse({}).success).toBe(true) + }) + + it('默认值:label 应为 "按钮"', () => { + const result = buttonComponentConfigSchema.parse({}) + expect(result.label).toBe('按钮') + }) + + it('默认值:buttonType 应为 trigger', () => { + const result = buttonComponentConfigSchema.parse({}) + expect(result.buttonType).toBe('trigger') + }) + + it('默认值:confirmRequired 应为 false', () => { + const result = buttonComponentConfigSchema.parse({}) + expect(result.confirmRequired).toBe(false) + }) + + it('buttonType 只能是 trigger / toggle / navigate', () => { + expect(buttonComponentConfigSchema.safeParse({ buttonType: 'trigger' }).success).toBe(true) + expect(buttonComponentConfigSchema.safeParse({ buttonType: 'toggle' }).success).toBe(true) + expect(buttonComponentConfigSchema.safeParse({ buttonType: 'navigate' }).success).toBe(true) + expect(buttonComponentConfigSchema.safeParse({ buttonType: 'invalid' }).success).toBe(false) + }) +}) + +describe('buttonComponentSchema — 按钮组件', () => { + it('合法按钮组件应通过验证', () => { + const data = createValidBase({ + type: 'button', + config: { + label: '启动', + buttonType: 'trigger', + targetMethod: 'startPump', + confirmRequired: true, + confirmMessage: '确认启动泵?', + }, + }) + expect(buttonComponentSchema.safeParse(data).success).toBe(true) + }) +}) + +// ============================================ +// 完整组件数据验证 +// ============================================ + +describe('完整组件数据验证', () => { + it('包含所有可选字段的完整组件应通过验证', () => { + const fullComponent = { + id: '550e8400-e29b-41d4-a716-446655440000', + type: 'text', + name: '温度显示', + position: { x: 50, y: 100 }, + size: { width: 200, height: 40 }, + zIndex: 5, + opacity: 0.8, + visible: true, + style: { + fill: { color: '#FFFFFF', opacity: 0.5 }, + border: { color: '#333333', width: 1, style: 'solid' as const, radius: 4 }, + text: { fontSize: 16, color: '#000000', align: 'center' as const }, + shadow: { color: '#000000', blur: 4, offsetX: 1, offsetY: 2, enabled: true }, + }, + events: [ + { + id: 'evt-1', + trigger: 'click', + action: { type: 'callMethod', payload: { method: 'reset' } }, + }, + ], + metadata: { groupId: 'g1', groupName: '温度组' }, + } + + const result = textComponentSchema.safeParse(fullComponent) + expect(result.success).toBe(true) + }) + + it('缺少必填字段 name 时应失败', () => { + const data = { + id: '550e8400-e29b-41d4-a716-446655440000', + type: 'rect', + position: { x: 0, y: 0 }, + size: { width: 100, height: 100 }, + } + expect(componentBaseSchema.safeParse(data).success).toBe(false) + }) + + it('缺少必填字段 position 时应失败', () => { + const data = { + id: '550e8400-e29b-41d4-a716-446655440000', + type: 'rect', + name: '测试', + size: { width: 100, height: 100 }, + } + expect(componentBaseSchema.safeParse(data).success).toBe(false) + }) + + it('缺少必填字段 size 时应失败', () => { + const data = { + id: '550e8400-e29b-41d4-a716-446655440000', + type: 'rect', + name: '测试', + position: { x: 0, y: 0 }, + } + expect(componentBaseSchema.safeParse(data).success).toBe(false) + }) +}) diff --git a/tests/unit/stores/userStore.test.ts b/tests/unit/stores/userStore.test.ts new file mode 100644 index 0000000..478f51f --- /dev/null +++ b/tests/unit/stores/userStore.test.ts @@ -0,0 +1,43 @@ +import { createPinia, setActivePinia } from 'pinia' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { useUserStore } from '@/stores/user' + +// Mock @vueuse/core 的 useLocalStorage,用 Vue 的 ref 替代以避免 localStorage 副作用 +vi.mock('@vueuse/core', async () => { + const { ref } = await import('vue') + return { + useLocalStorage: (_key: string, defaultValue: unknown) => ref(defaultValue), + } +}) + +describe('useUserStore — Token 管理', () => { + beforeEach(() => { + setActivePinia(createPinia()) + }) + + it('setToken 应正确设置 token 值', () => { + const store = useUserStore() + store.setToken('abc123') + expect(store.token).toBe('abc123') + }) + + it('setRToken 应正确设置 refreshToken 值', () => { + const store = useUserStore() + store.setRToken('refresh_xyz') + expect(store.rtoken).toBe('refresh_xyz') + }) + + it('setRTokenTime 应正确设置刷新时间戳', () => { + const store = useUserStore() + const timestamp = Date.now() + store.setRTokenTime(timestamp) + expect(store.rtokenTime).toBe(timestamp) + }) + + it('setDeviceType 应正确设置设备类型', () => { + const store = useUserStore() + store.setDeviceType('mobile') + expect(store.deviceType).toBe('mobile') + }) +}) diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..106e772 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ESNext", + "jsx": "preserve", + "jsxImportSource": "vue", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "useDefineForClassFields": true, + "module": "ESNext", + "moduleResolution": "bundler", + "paths": { + "@cslab-dcs/core": ["./packages/core/src"], + "@cslab-dcs/core/*": ["./packages/core/src/*"], + "@cslab-dcs/bridge": ["./packages/bridge/src"], + "@cslab-dcs/bridge/*": ["./packages/bridge/src/*"], + "@cslab-dcs/schema": ["./packages/schema/src"], + "@cslab-dcs/schema/*": ["./packages/schema/src/*"] + }, + "resolveJsonModule": true, + "strict": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "declaration": true, + "declarationMap": true, + "noEmit": true, + "sourceMap": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "skipLibCheck": true + }, + "exclude": ["node_modules", "dist", "out"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..865e891 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,30 @@ +import { resolve } from 'node:path' +import vue from '@vitejs/plugin-vue' +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': resolve(__dirname, 'packages/core/src'), + '@cslab-dcs/core': resolve(__dirname, 'packages/core/src'), + '@cslab-dcs/bridge': resolve(__dirname, 'packages/bridge/src'), + '@cslab-dcs/schema': resolve(__dirname, 'packages/schema/src'), + }, + }, + test: { + globals: true, + environment: 'happy-dom', + include: ['packages/**/tests/**/*.test.ts', 'tests/unit/**/*.test.ts'], + coverage: { + provider: 'v8', + include: [ + 'packages/core/src/composables/**', + 'packages/core/src/stores/**', + 'packages/core/src/components/editor/components/runtime.ts', + 'packages/schema/src/**', + ], + reporter: ['text', 'html'], + }, + }, +})